acp thread view: Floating editing message controls (#36283)
Prevents layout shift when focusing the editor Release Notes: - N/A --------- Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This commit is contained in:
parent
58f7006898
commit
e2db434920
3 changed files with 105 additions and 136 deletions
|
@ -67,6 +67,7 @@ impl EntryViewState {
|
||||||
self.project.clone(),
|
self.project.clone(),
|
||||||
self.thread_store.clone(),
|
self.thread_store.clone(),
|
||||||
self.text_thread_store.clone(),
|
self.text_thread_store.clone(),
|
||||||
|
"Edit message - @ to include context",
|
||||||
editor::EditorMode::AutoHeight {
|
editor::EditorMode::AutoHeight {
|
||||||
min_lines: 1,
|
min_lines: 1,
|
||||||
max_lines: None,
|
max_lines: None,
|
||||||
|
|
|
@ -71,6 +71,7 @@ impl MessageEditor {
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
text_thread_store: Entity<TextThreadStore>,
|
text_thread_store: Entity<TextThreadStore>,
|
||||||
|
placeholder: impl Into<Arc<str>>,
|
||||||
mode: EditorMode,
|
mode: EditorMode,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
|
@ -94,7 +95,7 @@ impl MessageEditor {
|
||||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||||
|
|
||||||
let mut editor = Editor::new(mode, buffer, None, window, cx);
|
let mut editor = Editor::new(mode, buffer, None, window, cx);
|
||||||
editor.set_placeholder_text("Message the agent - @ to include files", cx);
|
editor.set_placeholder_text(placeholder, cx);
|
||||||
editor.set_show_indent_guides(false, cx);
|
editor.set_show_indent_guides(false, cx);
|
||||||
editor.set_soft_wrap();
|
editor.set_soft_wrap();
|
||||||
editor.set_use_modal_editing(true);
|
editor.set_use_modal_editing(true);
|
||||||
|
@ -1276,6 +1277,7 @@ mod tests {
|
||||||
project.clone(),
|
project.clone(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
text_thread_store.clone(),
|
text_thread_store.clone(),
|
||||||
|
"Test",
|
||||||
EditorMode::AutoHeight {
|
EditorMode::AutoHeight {
|
||||||
min_lines: 1,
|
min_lines: 1,
|
||||||
max_lines: None,
|
max_lines: None,
|
||||||
|
@ -1473,6 +1475,7 @@ mod tests {
|
||||||
project.clone(),
|
project.clone(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
text_thread_store.clone(),
|
text_thread_store.clone(),
|
||||||
|
"Test",
|
||||||
EditorMode::AutoHeight {
|
EditorMode::AutoHeight {
|
||||||
max_lines: None,
|
max_lines: None,
|
||||||
min_lines: 1,
|
min_lines: 1,
|
||||||
|
|
|
@ -159,6 +159,7 @@ impl AcpThreadView {
|
||||||
project.clone(),
|
project.clone(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
text_thread_store.clone(),
|
text_thread_store.clone(),
|
||||||
|
"Message the agent - @ to include context",
|
||||||
editor::EditorMode::AutoHeight {
|
editor::EditorMode::AutoHeight {
|
||||||
min_lines: MIN_EDITOR_LINES,
|
min_lines: MIN_EDITOR_LINES,
|
||||||
max_lines: Some(MAX_EDITOR_LINES),
|
max_lines: Some(MAX_EDITOR_LINES),
|
||||||
|
@ -426,7 +427,9 @@ impl AcpThreadView {
|
||||||
match event {
|
match event {
|
||||||
MessageEditorEvent::Send => self.send(window, cx),
|
MessageEditorEvent::Send => self.send(window, cx),
|
||||||
MessageEditorEvent::Cancel => self.cancel_generation(cx),
|
MessageEditorEvent::Cancel => self.cancel_generation(cx),
|
||||||
MessageEditorEvent::Focus => {}
|
MessageEditorEvent::Focus => {
|
||||||
|
self.cancel_editing(&Default::default(), window, cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -742,44 +745,98 @@ impl AcpThreadView {
|
||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let primary = match &entry {
|
let primary = match &entry {
|
||||||
AgentThreadEntry::UserMessage(message) => div()
|
AgentThreadEntry::UserMessage(message) => {
|
||||||
.id(("user_message", entry_ix))
|
let Some(editor) = self
|
||||||
.py_4()
|
.entry_view_state
|
||||||
.px_2()
|
.read(cx)
|
||||||
.children(message.id.clone().and_then(|message_id| {
|
.entry(entry_ix)
|
||||||
message.checkpoint.as_ref()?.show.then(|| {
|
.and_then(|entry| entry.message_editor())
|
||||||
Button::new("restore-checkpoint", "Restore Checkpoint")
|
.cloned()
|
||||||
.icon(IconName::Undo)
|
else {
|
||||||
.icon_size(IconSize::XSmall)
|
return Empty.into_any_element();
|
||||||
.icon_position(IconPosition::Start)
|
};
|
||||||
.label_size(LabelSize::XSmall)
|
|
||||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
let editing = self.editing_message == Some(entry_ix);
|
||||||
this.rewind(&message_id, cx);
|
let editor_focus = editor.focus_handle(cx).is_focused(window);
|
||||||
}))
|
let focus_border = cx.theme().colors().border_focused;
|
||||||
})
|
|
||||||
}))
|
div()
|
||||||
.child(
|
.id(("user_message", entry_ix))
|
||||||
v_flex()
|
.py_4()
|
||||||
.p_3()
|
.px_2()
|
||||||
.gap_1p5()
|
.children(message.id.clone().and_then(|message_id| {
|
||||||
.rounded_lg()
|
message.checkpoint.as_ref()?.show.then(|| {
|
||||||
.shadow_md()
|
Button::new("restore-checkpoint", "Restore Checkpoint")
|
||||||
.bg(cx.theme().colors().editor_background)
|
.icon(IconName::Undo)
|
||||||
.border_1()
|
.icon_size(IconSize::XSmall)
|
||||||
.border_color(cx.theme().colors().border)
|
.icon_position(IconPosition::Start)
|
||||||
.text_xs()
|
.label_size(LabelSize::XSmall)
|
||||||
.children(
|
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||||
self.entry_view_state
|
this.rewind(&message_id, cx);
|
||||||
.read(cx)
|
}))
|
||||||
.entry(entry_ix)
|
})
|
||||||
.and_then(|entry| entry.message_editor())
|
}))
|
||||||
.map(|editor| {
|
.child(
|
||||||
self.render_sent_message_editor(entry_ix, editor, cx)
|
div()
|
||||||
.into_any_element()
|
.relative()
|
||||||
}),
|
.child(
|
||||||
),
|
div()
|
||||||
)
|
.p_3()
|
||||||
.into_any(),
|
.rounded_lg()
|
||||||
|
.shadow_md()
|
||||||
|
.bg(cx.theme().colors().editor_background)
|
||||||
|
.border_1()
|
||||||
|
.when(editing && !editor_focus, |this| this.border_dashed())
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
|
.map(|this|{
|
||||||
|
if editor_focus {
|
||||||
|
this.border_color(focus_border)
|
||||||
|
} else {
|
||||||
|
this.hover(|s| s.border_color(focus_border.opacity(0.8)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.text_xs()
|
||||||
|
.child(editor.clone().into_any_element()),
|
||||||
|
)
|
||||||
|
.when(editor_focus, |this|
|
||||||
|
this.child(
|
||||||
|
h_flex()
|
||||||
|
.absolute()
|
||||||
|
.top_neg_3p5()
|
||||||
|
.right_3()
|
||||||
|
.gap_1()
|
||||||
|
.rounded_sm()
|
||||||
|
.border_1()
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
|
.bg(cx.theme().colors().editor_background)
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(
|
||||||
|
IconButton::new("cancel", IconName::Close)
|
||||||
|
.icon_color(Color::Error)
|
||||||
|
.icon_size(IconSize::XSmall)
|
||||||
|
.on_click(cx.listener(Self::cancel_editing))
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
IconButton::new("regenerate", IconName::Return)
|
||||||
|
.icon_color(Color::Muted)
|
||||||
|
.icon_size(IconSize::XSmall)
|
||||||
|
.tooltip(Tooltip::text(
|
||||||
|
"Editing will restart the thread from this point."
|
||||||
|
))
|
||||||
|
.on_click(cx.listener({
|
||||||
|
let editor = editor.clone();
|
||||||
|
move |this, _, window, cx| {
|
||||||
|
this.regenerate(
|
||||||
|
entry_ix, &editor, window, cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
|
AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
|
||||||
let style = default_markdown_style(false, window, cx);
|
let style = default_markdown_style(false, window, cx);
|
||||||
let message_body = v_flex()
|
let message_body = v_flex()
|
||||||
|
@ -854,20 +911,12 @@ impl AcpThreadView {
|
||||||
if let Some(editing_index) = self.editing_message.as_ref()
|
if let Some(editing_index) = self.editing_message.as_ref()
|
||||||
&& *editing_index < entry_ix
|
&& *editing_index < entry_ix
|
||||||
{
|
{
|
||||||
let backdrop = div()
|
|
||||||
.id(("backdrop", entry_ix))
|
|
||||||
.size_full()
|
|
||||||
.absolute()
|
|
||||||
.inset_0()
|
|
||||||
.bg(cx.theme().colors().panel_background)
|
|
||||||
.opacity(0.8)
|
|
||||||
.block_mouse_except_scroll()
|
|
||||||
.on_click(cx.listener(Self::cancel_editing));
|
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.relative()
|
|
||||||
.child(primary)
|
.child(primary)
|
||||||
.child(backdrop)
|
.opacity(0.2)
|
||||||
|
.block_mouse_except_scroll()
|
||||||
|
.id("overlay")
|
||||||
|
.on_click(cx.listener(Self::cancel_editing))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
} else {
|
} else {
|
||||||
primary
|
primary
|
||||||
|
@ -2512,90 +2561,6 @@ impl AcpThreadView {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_sent_message_editor(
|
|
||||||
&self,
|
|
||||||
entry_ix: usize,
|
|
||||||
editor: &Entity<MessageEditor>,
|
|
||||||
cx: &Context<Self>,
|
|
||||||
) -> Div {
|
|
||||||
v_flex().w_full().gap_2().child(editor.clone()).when(
|
|
||||||
self.editing_message == Some(entry_ix),
|
|
||||||
|el| {
|
|
||||||
el.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::Warning)
|
|
||||||
.color(Color::Warning)
|
|
||||||
.size(IconSize::XSmall),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Label::new("Editing will restart the thread from this point.")
|
|
||||||
.color(Color::Muted)
|
|
||||||
.size(LabelSize::XSmall),
|
|
||||||
)
|
|
||||||
.child(self.render_sent_message_editor_buttons(entry_ix, editor, cx)),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_sent_message_editor_buttons(
|
|
||||||
&self,
|
|
||||||
entry_ix: usize,
|
|
||||||
editor: &Entity<MessageEditor>,
|
|
||||||
cx: &Context<Self>,
|
|
||||||
) -> Div {
|
|
||||||
h_flex()
|
|
||||||
.gap_0p5()
|
|
||||||
.flex_1()
|
|
||||||
.justify_end()
|
|
||||||
.child(
|
|
||||||
IconButton::new("cancel-edit-message", IconName::Close)
|
|
||||||
.shape(ui::IconButtonShape::Square)
|
|
||||||
.icon_color(Color::Error)
|
|
||||||
.icon_size(IconSize::Small)
|
|
||||||
.tooltip({
|
|
||||||
let focus_handle = editor.focus_handle(cx);
|
|
||||||
move |window, cx| {
|
|
||||||
Tooltip::for_action_in(
|
|
||||||
"Cancel Edit",
|
|
||||||
&menu::Cancel,
|
|
||||||
&focus_handle,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on_click(cx.listener(Self::cancel_editing)),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
IconButton::new("confirm-edit-message", IconName::Return)
|
|
||||||
.disabled(editor.read(cx).is_empty(cx))
|
|
||||||
.shape(ui::IconButtonShape::Square)
|
|
||||||
.icon_color(Color::Muted)
|
|
||||||
.icon_size(IconSize::Small)
|
|
||||||
.tooltip({
|
|
||||||
let focus_handle = editor.focus_handle(cx);
|
|
||||||
move |window, cx| {
|
|
||||||
Tooltip::for_action_in(
|
|
||||||
"Regenerate",
|
|
||||||
&menu::Confirm,
|
|
||||||
&focus_handle,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on_click(cx.listener({
|
|
||||||
let editor = editor.clone();
|
|
||||||
move |this, _, window, cx| {
|
|
||||||
this.regenerate(entry_ix, &editor, window, cx);
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
if self.thread().map_or(true, |thread| {
|
if self.thread().map_or(true, |thread| {
|
||||||
thread.read(cx).status() == ThreadStatus::Idle
|
thread.read(cx).status() == ThreadStatus::Idle
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue