Allow including conversation when triggering inline assist

This commit is contained in:
Antonio Scandurra 2023-08-28 16:36:07 +02:00
parent ccec59337a
commit c2b60df5af
3 changed files with 189 additions and 26 deletions

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import { text, border, background, foreground, TextStyle } from "./components" 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 { tab_bar_button } from "../component/tab_bar_button"
import { StyleSets, useTheme } from "../theme" import { StyleSets, useTheme } from "../theme"
@ -80,6 +80,62 @@ export default function assistant(): any {
}, },
}, },
pending_edit_background: background(theme.highest, "positive"), 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: { message_header: {
margin: { bottom: 4, top: 4 }, margin: { bottom: 4, top: 4 },