Compare commits

...

2 Commits

Author SHA1 Message Date
Mikayla Maki
4ac098ce0a wip 2025-12-04 14:20:55 -08:00
Mikayla Maki
8ff95c153e Refactor the inline prompt editor to use a component
co-authored-by: Zed Agent
2025-12-04 11:28:23 -08:00

View File

@@ -60,8 +60,7 @@ impl<T: 'static> EventEmitter<PromptEditorEvent> for PromptEditor<T> {}
impl<T: 'static> Render for PromptEditor<T> {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
let mut buttons = Vec::new();
let mut action_buttons = Vec::new();
const RIGHT_PADDING: Pixels = px(9.);
@@ -74,7 +73,7 @@ impl<T: 'static> Render for PromptEditor<T> {
let codegen = codegen.read(cx);
if codegen.alternative_count(cx) > 1 {
buttons.push(self.render_cycle_controls(codegen, cx));
action_buttons.push(self.render_cycle_controls(codegen, cx));
}
let editor_margins = editor_margins.lock();
@@ -85,10 +84,7 @@ impl<T: 'static> Render for PromptEditor<T> {
(left_gutter_width, right_padding)
}
PromptEditorMode::Terminal { .. } => {
// Give the equivalent of the same left-padding that we're using on the right
(Pixels::from(40.0), Pixels::from(24.))
}
PromptEditorMode::Terminal { .. } => (Pixels::from(40.0), Pixels::from(24.)),
};
let bottom_padding = match &self.mode {
@@ -96,7 +92,7 @@ impl<T: 'static> Render for PromptEditor<T> {
PromptEditorMode::Terminal { .. } => rems_from_px(8.0),
};
buttons.extend(self.render_buttons(window, cx));
action_buttons.extend(self.render_buttons(window, cx));
let menu_visible = self.is_completions_menu_visible(cx);
let add_context_button = IconButton::new("add-context", IconName::AtSign)
@@ -109,93 +105,48 @@ impl<T: 'static> Render for PromptEditor<T> {
})
.on_click(cx.listener(move |this, _, window, cx| {
this.trigger_completion_menu(window, cx);
}));
}))
.into_any_element();
let close_button = self.render_close_button(cx);
let error_message = if let CodegenStatus::Error(error) = self.codegen_status(cx) {
Some(SharedString::from(error.to_string()))
} else {
None
};
let editor = self.render_editor(window, cx);
let model_selector = self.model_selector.clone().into_any_element();
v_flex()
.key_context("PromptEditor")
.capture_action(cx.listener(Self::paste))
.bg(cx.theme().colors().editor_background)
.block_mouse_except_scroll()
.gap_0p5()
.border_y_1()
.border_color(cx.theme().status().info_border)
.size_full()
.pt_0p5()
.pb(bottom_padding)
.pr(right_padding)
.cursor(CursorStyle::Arrow)
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
this.model_selector
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
}))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.capture_action(cx.listener(Self::cycle_prev))
.capture_action(cx.listener(Self::cycle_next))
.child(
h_flex()
.items_start()
.cursor(CursorStyle::Arrow)
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
this.model_selector
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
}))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.capture_action(cx.listener(Self::cycle_prev))
.capture_action(cx.listener(Self::cycle_next))
.child(
WithRemSize::new(ui_font_size)
.flex()
.flex_row()
.flex_shrink_0()
.items_center()
.h_full()
.w(left_gutter_width)
.justify_center()
.gap_2()
.child(self.render_close_button(cx))
.map(|el| {
let CodegenStatus::Error(error) = self.codegen_status(cx) else {
return el;
};
let error_message = SharedString::from(error.to_string());
el.child(
div()
.id("error")
.tooltip(Tooltip::text(error_message))
.child(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
),
)
}),
)
.child(
h_flex()
.w_full()
.justify_between()
.child(div().flex_1().child(self.render_editor(window, cx)))
.child(
WithRemSize::new(ui_font_size)
.flex()
.flex_row()
.items_center()
.gap_1()
.children(buttons),
),
),
)
.child(
WithRemSize::new(ui_font_size)
.flex()
.flex_row()
.items_center()
.child(h_flex().flex_shrink_0().w(left_gutter_width))
.child(
h_flex()
.w_full()
.pl_1()
.items_start()
.justify_between()
.child(add_context_button)
.child(self.model_selector.clone()),
),
PromptEditorLayout::new(
editor,
close_button,
action_buttons,
add_context_button,
model_selector,
)
.error_message(error_message)
.left_gutter_width(left_gutter_width)
.right_padding(right_padding)
.bottom_padding(bottom_padding),
)
}
}
@@ -1174,6 +1125,237 @@ impl GenerationMode {
}
}
/// A stateless layout component for the inline prompt editor.
///
/// This component handles the visual layout of the prompt editor UI without
/// any behavior. It's used by both `PromptEditor` (with interactive elements)
/// and the component preview (with static elements).
#[derive(IntoElement)]
pub struct PromptEditorLayout {
/// The editor element to display
editor: AnyElement,
/// Close button element (left gutter)
close_button: AnyElement,
/// Optional error message to display (left gutter, shown as error icon when present)
error_message: Option<SharedString>,
/// Action buttons (right side: start/stop/accept/restart + cycle controls)
action_buttons: Vec<AnyElement>,
/// Add context button (bottom left, @ button)
add_context_button: AnyElement,
/// Model selector element (bottom right)
model_selector: AnyElement,
/// Left gutter width for alignment
left_gutter_width: Pixels,
/// Right padding
right_padding: Pixels,
/// Bottom padding
bottom_padding: Rems,
}
impl PromptEditorLayout {
pub fn new(
editor: AnyElement,
close_button: AnyElement,
action_buttons: Vec<AnyElement>,
add_context_button: AnyElement,
model_selector: AnyElement,
) -> Self {
Self {
editor,
close_button,
error_message: None,
action_buttons,
add_context_button,
model_selector,
left_gutter_width: px(40.0),
right_padding: px(9.0),
bottom_padding: rems_from_px(2.0),
}
}
pub fn error_message(mut self, error_message: impl Into<Option<SharedString>>) -> Self {
self.error_message = error_message.into();
self
}
pub fn left_gutter_width(mut self, width: Pixels) -> Self {
self.left_gutter_width = width;
self
}
pub fn right_padding(mut self, padding: Pixels) -> Self {
self.right_padding = padding;
self
}
pub fn bottom_padding(mut self, padding: Rems) -> Self {
self.bottom_padding = padding;
self
}
/// Creates a PromptEditorLayout for preview/static rendering.
///
/// This constructor handles creating all the static (non-interactive) buttons
/// based on the codegen status, mode, and other parameters. It's used by the
/// component preview system.
pub fn preview(
editor: AnyElement,
codegen_status: CodegenStatus,
mode: GenerationMode,
edited_since_done: bool,
cx: &App,
) -> Self {
// Create action buttons based on status
let action_buttons = match codegen_status {
CodegenStatus::Idle => {
vec![
Button::new("start", mode.start_label())
.label_size(LabelSize::Small)
.icon(IconName::Return)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.into_any_element(),
]
}
CodegenStatus::Pending => vec![
IconButton::new("stop", IconName::Stop)
.icon_color(Color::Error)
.shape(IconButtonShape::Square)
.into_any_element(),
],
CodegenStatus::Done => {
if edited_since_done {
vec![
IconButton::new("restart", IconName::RotateCw)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.into_any_element(),
]
} else {
vec![
IconButton::new("accept", IconName::Check)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.into_any_element(),
]
}
}
CodegenStatus::Error(_) => {
vec![
IconButton::new("restart", IconName::RotateCw)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.into_any_element(),
]
}
};
let close_button = IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.into_any_element();
let add_context_button = IconButton::new("add-context", IconName::AtSign)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.into_any_element();
let model_selector = div()
.text_color(cx.theme().colors().text_muted)
.child("Model Selector")
.into_any_element();
let error_message = if let CodegenStatus::Error(error) = codegen_status {
Some(SharedString::from(error.to_string()))
} else {
None
};
Self::new(
editor,
close_button,
action_buttons,
add_context_button,
model_selector,
)
.error_message(error_message)
}
}
impl RenderOnce for PromptEditorLayout {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
v_flex()
.bg(cx.theme().colors().editor_background)
.gap_0p5()
.border_y_1()
.border_color(cx.theme().status().info_border)
.size_full()
.pt_0p5()
.pb(self.bottom_padding)
.pr(self.right_padding)
.child(
h_flex()
.items_start()
.child(
WithRemSize::new(ui_font_size)
.flex()
.flex_row()
.flex_shrink_0()
.items_center()
.h_full()
.w(self.left_gutter_width)
.justify_center()
.gap_2()
.child(self.close_button)
.when_some(self.error_message, |el, error_message| {
el.child(
div()
.id("error")
.tooltip(Tooltip::text(error_message))
.child(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
),
)
}),
)
.child(
h_flex()
.w_full()
.justify_between()
.child(div().flex_1().child(self.editor))
.child(
WithRemSize::new(ui_font_size)
.flex()
.flex_row()
.items_center()
.gap_1()
.children(self.action_buttons),
),
),
)
.child(
WithRemSize::new(ui_font_size)
.flex()
.flex_row()
.items_center()
.child(h_flex().flex_shrink_0().w(self.left_gutter_width))
.child(
h_flex()
.w_full()
.pl_1()
.items_start()
.justify_between()
.child(self.add_context_button)
.child(self.model_selector),
),
)
}
}
/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
#[derive(Clone, Debug)]
struct MessageCrease {
@@ -1229,3 +1411,144 @@ fn insert_message_creases(
editor.fold_creases(creases, false, window, cx);
ids
}
mod preview {
use component::{Component, ComponentScope, example_group_with_title, single_example};
use editor::Editor;
use gpui::{AnyElement, App, Window};
use ui::prelude::*;
use super::{CodegenStatus, GenerationMode, PromptEditorLayout};
// View this component preview using `workspace: open component-preview`
#[derive(IntoElement, RegisterComponent)]
struct PromptEditorPreview;
impl Component for PromptEditorPreview {
fn scope() -> ComponentScope {
ComponentScope::Agent
}
fn name() -> &'static str {
"Inline Prompt Editor"
}
fn sort_name() -> &'static str {
"AgentInlinePromptEditor"
}
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let editor = window.use_state(cx, |window, cx| {
let mut editor = Editor::single_line(window, cx);
editor.set_placeholder_text("How can I help?", window, cx);
editor
});
Some(
v_flex()
.gap_6()
.child(example_group_with_title(
"Idle State",
vec![
single_example(
"Generate",
div()
.w(px(600.))
.child(PromptEditorLayout::preview(
editor.clone().into_any_element(),
CodegenStatus::Idle,
GenerationMode::Generate,
false,
cx,
))
.into_any_element(),
),
single_example(
"Transform",
div()
.w(px(600.))
.child(PromptEditorLayout::preview(
editor.clone().into_any_element(),
CodegenStatus::Idle,
GenerationMode::Transform,
false,
cx,
))
.into_any_element(),
),
],
))
.child(example_group_with_title(
"Pending State",
vec![single_example(
"Stop Button",
div()
.w(px(600.))
.child(PromptEditorLayout::preview(
editor.clone().into_any_element(),
CodegenStatus::Pending,
GenerationMode::Generate,
false,
cx,
))
.into_any_element(),
)],
))
.child(example_group_with_title(
"Done State",
vec![
single_example(
"Accept Button",
div()
.w(px(600.))
.child(PromptEditorLayout::preview(
editor.clone().into_any_element(),
CodegenStatus::Done,
GenerationMode::Generate,
false,
cx,
))
.into_any_element(),
),
single_example(
"Edited Since Done (Restart)",
div()
.w(px(600.))
.child(PromptEditorLayout::preview(
editor.clone().into_any_element(),
CodegenStatus::Done,
GenerationMode::Generate,
true,
cx,
))
.into_any_element(),
),
],
))
.child(example_group_with_title(
"Error State",
vec![single_example(
"Error Indicator with Restart",
div()
.w(px(600.))
.child(PromptEditorLayout::preview(
editor.into_any_element(),
CodegenStatus::Error(anyhow::anyhow!("Example error message")),
GenerationMode::Generate,
false,
cx,
))
.into_any_element(),
)],
))
.into_any_element(),
)
}
}
impl RenderOnce for PromptEditorPreview {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
div().child("Inline Prompt Editor Preview")
}
}
}