Allow including conversation when triggering inline assist

Antonio Scandurra created

Change summary

crates/ai/src/assistant.rs         | 156 ++++++++++++++++++++++++++-----
crates/theme/src/theme.rs          |   1 
styles/src/style_tree/assistant.ts |  58 +++++++++++
3 files changed, 189 insertions(+), 26 deletions(-)

Detailed changes

crates/ai/src/assistant.rs 🔗

@@ -19,12 +19,16 @@ use fs::Fs;
 use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
 use gpui::{
     actions,
-    elements::*,
+    elements::{
+        ChildView, Component, Empty, Flex, Label, MouseEventHandler, ParentElement, SafeStylable,
+        Stack, Svg, Text, UniformList, UniformListState,
+    },
     fonts::HighlightStyle,
     geometry::vector::{vec2f, Vector2F},
     platform::{CursorStyle, MouseButton},
-    Action, AppContext, AsyncAppContext, ClipboardItem, Entity, ModelContext, ModelHandle,
-    Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
+    Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelContext,
+    ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    WindowContext,
 };
 use language::{
     language_settings::SoftWrap, Buffer, LanguageRegistry, Point, Rope, ToOffset as _,
@@ -33,7 +37,7 @@ use language::{
 use search::BufferSearchBar;
 use settings::SettingsStore;
 use std::{
-    cell::RefCell,
+    cell::{Cell, RefCell},
     cmp, env,
     fmt::Write,
     iter,
@@ -43,7 +47,10 @@ use std::{
     sync::Arc,
     time::Duration,
 };
-use theme::AssistantStyle;
+use theme::{
+    components::{action_button::Button, ComponentExt},
+    AssistantStyle,
+};
 use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel},
@@ -61,7 +68,8 @@ actions!(
         QuoteSelection,
         ToggleFocus,
         ResetKey,
-        InlineAssist
+        InlineAssist,
+        ToggleIncludeConversation,
     ]
 );
 
@@ -97,6 +105,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(AssistantPanel::cancel_last_inline_assist);
     cx.add_action(InlineAssistant::confirm);
     cx.add_action(InlineAssistant::cancel);
+    cx.add_action(InlineAssistant::toggle_include_conversation);
 }
 
 #[derive(Debug)]
@@ -129,6 +138,7 @@ pub struct AssistantPanel {
     next_inline_assist_id: usize,
     pending_inline_assists: HashMap<usize, PendingInlineAssist>,
     pending_inline_assist_ids_by_editor: HashMap<WeakViewHandle<Editor>, Vec<usize>>,
+    include_conversation_in_next_inline_assist: bool,
     _watch_saved_conversations: Task<Result<()>>,
 }
 
@@ -195,6 +205,7 @@ impl AssistantPanel {
                         next_inline_assist_id: 0,
                         pending_inline_assists: Default::default(),
                         pending_inline_assist_ids_by_editor: Default::default(),
+                        include_conversation_in_next_inline_assist: false,
                         _watch_saved_conversations,
                     };
 
@@ -270,12 +281,15 @@ impl AssistantPanel {
             editor.set_placeholder_text(placeholder, cx);
             editor
         });
+        let measurements = Rc::new(Cell::new(BlockMeasurements::default()));
         let inline_assistant = cx.add_view(|cx| {
             let assistant = InlineAssistant {
                 id: inline_assist_id,
                 prompt_editor,
                 confirmed: false,
                 has_focus: false,
+                include_conversation: self.include_conversation_in_next_inline_assist,
+                measurements: measurements.clone(),
             };
             cx.focus_self();
             assistant
@@ -292,13 +306,11 @@ impl AssistantPanel {
                     render: Arc::new({
                         let inline_assistant = inline_assistant.clone();
                         move |cx: &mut BlockContext| {
-                            let theme = theme::current(cx);
-                            ChildView::new(&inline_assistant, cx)
-                                .contained()
-                                .with_padding_left(cx.anchor_x)
-                                .contained()
-                                .with_style(theme.assistant.inline.container)
-                                .into_any()
+                            measurements.set(BlockMeasurements {
+                                anchor_x: cx.anchor_x,
+                                gutter_width: cx.gutter_width,
+                            });
+                            ChildView::new(&inline_assistant, cx).into_any()
                         }
                     }),
                     disposition: if selection.reversed {
@@ -375,8 +387,11 @@ impl AssistantPanel {
     ) {
         let assist_id = inline_assistant.read(cx).id;
         match event {
-            InlineAssistantEvent::Confirmed { prompt } => {
-                self.confirm_inline_assist(assist_id, prompt, cx);
+            InlineAssistantEvent::Confirmed {
+                prompt,
+                include_conversation,
+            } => {
+                self.confirm_inline_assist(assist_id, prompt, *include_conversation, cx);
             }
             InlineAssistantEvent::Canceled => {
                 self.close_inline_assist(assist_id, true, cx);
@@ -470,14 +485,24 @@ impl AssistantPanel {
         &mut self,
         inline_assist_id: usize,
         user_prompt: &str,
+        include_conversation: bool,
         cx: &mut ViewContext<Self>,
     ) {
+        self.include_conversation_in_next_inline_assist = include_conversation;
+
         let api_key = if let Some(api_key) = self.api_key.borrow().clone() {
             api_key
         } else {
             return;
         };
 
+        let conversation = if include_conversation {
+            self.active_editor()
+                .map(|editor| editor.read(cx).conversation.clone())
+        } else {
+            None
+        };
+
         let pending_assist =
             if let Some(pending_assist) = self.pending_inline_assists.get_mut(&inline_assist_id) {
                 pending_assist
@@ -626,14 +651,25 @@ impl AssistantPanel {
         )
         .unwrap();
 
-        let request = OpenAIRequest {
+        let mut request = OpenAIRequest {
             model: model.full_name().into(),
-            messages: vec![RequestMessage {
-                role: Role::User,
-                content: prompt,
-            }],
+            messages: Vec::new(),
             stream: true,
         };
+        if let Some(conversation) = conversation {
+            let conversation = conversation.read(cx);
+            let buffer = conversation.buffer.read(cx);
+            request.messages.extend(
+                conversation
+                    .messages(cx)
+                    .map(|message| message.to_open_ai_message(buffer)),
+            );
+        }
+
+        request.messages.push(RequestMessage {
+            role: Role::User,
+            content: prompt,
+        });
         let response = stream_completion(api_key, cx.background().clone(), request);
         let editor = editor.downgrade();
 
@@ -2799,7 +2835,10 @@ impl Message {
 }
 
 enum InlineAssistantEvent {
-    Confirmed { prompt: String },
+    Confirmed {
+        prompt: String,
+        include_conversation: bool,
+    },
     Canceled,
     Dismissed,
 }
@@ -2815,6 +2854,8 @@ struct InlineAssistant {
     prompt_editor: ViewHandle<Editor>,
     confirmed: bool,
     has_focus: bool,
+    include_conversation: bool,
+    measurements: Rc<Cell<BlockMeasurements>>,
 }
 
 impl Entity for InlineAssistant {
@@ -2827,9 +2868,55 @@ impl View for InlineAssistant {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        ChildView::new(&self.prompt_editor, cx)
-            .aligned()
-            .left()
+        let theme = theme::current(cx);
+
+        Flex::row()
+            .with_child(
+                Button::action(ToggleIncludeConversation)
+                    .with_tooltip("Include Conversation", theme.tooltip.clone())
+                    .with_id(self.id)
+                    .with_contents(theme::components::svg::Svg::new("icons/ai.svg"))
+                    .toggleable(self.include_conversation)
+                    .with_style(theme.assistant.inline.include_conversation.clone())
+                    .element()
+                    .aligned()
+                    .constrained()
+                    .dynamically({
+                        let measurements = self.measurements.clone();
+                        move |constraint, _, _| {
+                            let measurements = measurements.get();
+                            SizeConstraint {
+                                min: vec2f(measurements.gutter_width, constraint.min.y()),
+                                max: vec2f(measurements.gutter_width, constraint.max.y()),
+                            }
+                        }
+                    }),
+            )
+            .with_child(Empty::new().constrained().dynamically({
+                let measurements = self.measurements.clone();
+                move |constraint, _, _| {
+                    let measurements = measurements.get();
+                    SizeConstraint {
+                        min: vec2f(
+                            measurements.anchor_x - measurements.gutter_width,
+                            constraint.min.y(),
+                        ),
+                        max: vec2f(
+                            measurements.anchor_x - measurements.gutter_width,
+                            constraint.max.y(),
+                        ),
+                    }
+                }
+            }))
+            .with_child(
+                ChildView::new(&self.prompt_editor, cx)
+                    .aligned()
+                    .left()
+                    .flex(1., true),
+            )
+            .contained()
+            .with_style(theme.assistant.inline.container)
+            .into_any()
             .into_any()
     }
 
@@ -2862,10 +2949,29 @@ impl InlineAssistant {
                     cx,
                 );
             });
-            cx.emit(InlineAssistantEvent::Confirmed { prompt });
+            cx.emit(InlineAssistantEvent::Confirmed {
+                prompt,
+                include_conversation: self.include_conversation,
+            });
             self.confirmed = true;
         }
     }
+
+    fn toggle_include_conversation(
+        &mut self,
+        _: &ToggleIncludeConversation,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.include_conversation = !self.include_conversation;
+        cx.notify();
+    }
+}
+
+// This wouldn't need to exist if we could pass parameters when rendering child views.
+#[derive(Copy, Clone, Default)]
+struct BlockMeasurements {
+    anchor_x: f32,
+    gutter_width: f32,
 }
 
 struct PendingInlineAssist {

crates/theme/src/theme.rs 🔗

@@ -1160,6 +1160,7 @@ pub struct InlineAssistantStyle {
     pub editor: FieldEditor,
     pub disabled_editor: FieldEditor,
     pub pending_edit_background: Color,
+    pub include_conversation: ToggleIconButtonStyle,
 }
 
 #[derive(Clone, Deserialize, Default, JsonSchema)]

styles/src/style_tree/assistant.ts 🔗

@@ -1,5 +1,5 @@
 import { text, border, background, foreground, TextStyle } from "./components"
-import { Interactive, interactive } from "../element"
+import { Interactive, interactive, toggleable } from "../element"
 import { tab_bar_button } from "../component/tab_bar_button"
 import { StyleSets, useTheme } from "../theme"
 
@@ -80,6 +80,62 @@ export default function assistant(): any {
                 },
             },
             pending_edit_background: background(theme.highest, "positive"),
+            include_conversation: toggleable({
+                base: interactive({
+                    base: {
+                        icon_size: 12,
+                        color: foreground(theme.highest, "variant"),
+
+                        button_width: 12,
+                        background: background(theme.highest, "on"),
+                        corner_radius: 2,
+                        border: {
+                            width: 1., color: background(theme.highest, "on")
+                        },
+                        padding: {
+                            left: 4,
+                            right: 4,
+                            top: 4,
+                            bottom: 4,
+                        },
+                    },
+                    state: {
+                        hovered: {
+                            ...text(theme.highest, "mono", "variant", "hovered"),
+                            background: background(theme.highest, "on", "hovered"),
+                            border: {
+                                width: 1., color: background(theme.highest, "on", "hovered")
+                            },
+                        },
+                        clicked: {
+                            ...text(theme.highest, "mono", "variant", "pressed"),
+                            background: background(theme.highest, "on", "pressed"),
+                            border: {
+                                width: 1., color: background(theme.highest, "on", "pressed")
+                            },
+                        },
+                    },
+                }),
+                state: {
+                    active: {
+                        default: {
+                            icon_size: 12,
+                            button_width: 12,
+                            color: foreground(theme.highest, "variant"),
+                            background: background(theme.highest, "accent"),
+                            border: border(theme.highest, "accent"),
+                        },
+                        hovered: {
+                            background: background(theme.highest, "accent", "hovered"),
+                            border: border(theme.highest, "accent", "hovered"),
+                        },
+                        clicked: {
+                            background: background(theme.highest, "accent", "pressed"),
+                            border: border(theme.highest, "accent", "pressed"),
+                        },
+                    },
+                },
+            }),
         },
         message_header: {
             margin: { bottom: 4, top: 4 },