Refine UX for assistants (#13502)

<img width="1652" alt="image"
src="https://github.com/zed-industries/zed/assets/482957/376d1915-1e15-4d6c-966e-48f55f7cb249">


Release Notes:

- N/A
This commit is contained in:
Antonio Scandurra 2024-06-25 13:41:55 +02:00 committed by GitHub
parent 86cd87e993
commit a4cdca5141
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 419 additions and 203 deletions

View File

@ -10,14 +10,14 @@ mod search;
mod slash_command; mod slash_command;
mod streaming_diff; mod streaming_diff;
pub use assistant_panel::AssistantPanel; pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
use assistant_settings::{AnthropicModel, AssistantSettings, CloudModel, OllamaModel, OpenAiModel}; use assistant_settings::{AnthropicModel, AssistantSettings, CloudModel, OllamaModel, OpenAiModel};
use assistant_slash_command::SlashCommandRegistry; use assistant_slash_command::SlashCommandRegistry;
use client::{proto, Client}; use client::{proto, Client};
use command_palette_hooks::CommandPaletteFilter; use command_palette_hooks::CommandPaletteFilter;
pub(crate) use completion_provider::*; pub(crate) use completion_provider::*;
pub(crate) use context_store::*; pub(crate) use context_store::*;
use fs::Fs;
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal}; use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
pub(crate) use inline_assistant::*; pub(crate) use inline_assistant::*;
pub(crate) use model_selector::*; pub(crate) use model_selector::*;
@ -264,7 +264,7 @@ impl Assistant {
} }
} }
pub fn init(client: Arc<Client>, cx: &mut AppContext) { pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) {
cx.set_global(Assistant::default()); cx.set_global(Assistant::default());
AssistantSettings::register(cx); AssistantSettings::register(cx);
@ -288,7 +288,7 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
assistant_slash_command::init(cx); assistant_slash_command::init(cx);
register_slash_commands(cx); register_slash_commands(cx);
assistant_panel::init(cx); assistant_panel::init(cx);
inline_assistant::init(client.telemetry().clone(), cx); inline_assistant::init(fs.clone(), client.telemetry().clone(), cx);
RustdocStore::init_global(cx); RustdocStore::init_global(cx);
CommandPaletteFilter::update_global(cx, |filter, _cx| { CommandPaletteFilter::update_global(cx, |filter, _cx| {
@ -324,6 +324,24 @@ fn register_slash_commands(cx: &mut AppContext) {
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false); slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
} }
pub fn humanize_token_count(count: usize) -> String {
match count {
0..=999 => count.to_string(),
1000..=9999 => {
let thousands = count / 1000;
let hundreds = (count % 1000 + 50) / 100;
if hundreds == 0 {
format!("{}k", thousands)
} else if hundreds == 10 {
format!("{}k", thousands + 1)
} else {
format!("{}.{}k", thousands, hundreds)
}
}
_ => format!("{}k", (count + 500) / 1000),
}
}
#[cfg(test)] #[cfg(test)]
#[ctor::ctor] #[ctor::ctor]
fn init_logger() { fn init_logger() {

View File

@ -1,5 +1,6 @@
use crate::{ use crate::{
assistant_settings::{AssistantDockPosition, AssistantSettings}, assistant_settings::{AssistantDockPosition, AssistantSettings},
humanize_token_count,
prompt_library::open_prompt_library, prompt_library::open_prompt_library,
search::*, search::*,
slash_command::{ slash_command::{
@ -89,6 +90,10 @@ pub fn init(cx: &mut AppContext) {
.detach(); .detach();
} }
pub enum AssistantPanelEvent {
ContextEdited,
}
pub struct AssistantPanel { pub struct AssistantPanel {
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
width: Option<Pixels>, width: Option<Pixels>,
@ -360,11 +365,11 @@ impl AssistantPanel {
return; return;
} }
let Some(assistant) = workspace.panel::<AssistantPanel>(cx) else { let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
return; return;
}; };
let context_editor = assistant let context_editor = assistant_panel
.read(cx) .read(cx)
.active_context_editor() .active_context_editor()
.and_then(|editor| { .and_then(|editor| {
@ -391,25 +396,37 @@ impl AssistantPanel {
return; return;
}; };
if assistant.update(cx, |assistant, cx| assistant.is_authenticated(cx)) { if assistant_panel.update(cx, |panel, cx| panel.is_authenticated(cx)) {
InlineAssistant::update_global(cx, |assistant, cx| { InlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist( assistant.assist(
&active_editor, &active_editor,
Some(cx.view().downgrade()), Some(cx.view().downgrade()),
include_context, include_context.then_some(&assistant_panel),
cx, cx,
) )
}) })
} else { } else {
let assistant = assistant.downgrade(); let assistant_panel = assistant_panel.downgrade();
cx.spawn(|workspace, mut cx| async move { cx.spawn(|workspace, mut cx| async move {
assistant assistant_panel
.update(&mut cx, |assistant, cx| assistant.authenticate(cx))? .update(&mut cx, |assistant, cx| assistant.authenticate(cx))?
.await?; .await?;
if assistant.update(&mut cx, |assistant, cx| assistant.is_authenticated(cx))? { if assistant_panel
.update(&mut cx, |assistant, cx| assistant.is_authenticated(cx))?
{
cx.update(|cx| { cx.update(|cx| {
let assistant_panel = if include_context {
assistant_panel.upgrade()
} else {
None
};
InlineAssistant::update_global(cx, |assistant, cx| { InlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&active_editor, Some(workspace), include_context, cx) assistant.assist(
&active_editor,
Some(workspace),
assistant_panel.as_ref(),
cx,
)
}) })
})? })?
} else { } else {
@ -460,7 +477,7 @@ impl AssistantPanel {
_subscriptions: subscriptions, _subscriptions: subscriptions,
}); });
self.show_saved_contexts = false; self.show_saved_contexts = false;
cx.emit(AssistantPanelEvent::ContextEdited);
cx.notify(); cx.notify();
} }
@ -472,6 +489,7 @@ impl AssistantPanel {
) { ) {
match event { match event {
ContextEditorEvent::TabContentChanged => cx.notify(), ContextEditorEvent::TabContentChanged => cx.notify(),
ContextEditorEvent::Edited => cx.emit(AssistantPanelEvent::ContextEdited),
} }
} }
@ -863,18 +881,33 @@ impl AssistantPanel {
context: &Model<Context>, context: &Model<Context>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Option<impl IntoElement> { ) -> Option<impl IntoElement> {
let remaining_tokens = context.read(cx).remaining_tokens(cx)?; let model = CompletionProvider::global(cx).model();
let remaining_tokens_color = if remaining_tokens <= 0 { let token_count = context.read(cx).token_count()?;
let max_token_count = model.max_token_count();
let remaining_tokens = max_token_count as isize - token_count as isize;
let token_count_color = if remaining_tokens <= 0 {
Color::Error Color::Error
} else if remaining_tokens <= 500 { } else if token_count as f32 / max_token_count as f32 >= 0.8 {
Color::Warning Color::Warning
} else { } else {
Color::Muted Color::Muted
}; };
Some( Some(
Label::new(remaining_tokens.to_string()) h_flex()
.size(LabelSize::Small) .gap_0p5()
.color(remaining_tokens_color), .child(
Label::new(humanize_token_count(token_count))
.size(LabelSize::Small)
.color(token_count_color),
)
.child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
.child(
Label::new(humanize_token_count(max_token_count))
.size(LabelSize::Small)
.color(Color::Muted),
),
) )
} }
} }
@ -978,6 +1011,7 @@ impl Panel for AssistantPanel {
} }
impl EventEmitter<PanelEvent> for AssistantPanel {} impl EventEmitter<PanelEvent> for AssistantPanel {}
impl EventEmitter<AssistantPanelEvent> for AssistantPanel {}
impl FocusableView for AssistantPanel { impl FocusableView for AssistantPanel {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
@ -1538,11 +1572,6 @@ impl Context {
} }
} }
fn remaining_tokens(&self, cx: &AppContext) -> Option<isize> {
let model = CompletionProvider::global(cx).model();
Some(model.max_token_count() as isize - self.token_count? as isize)
}
fn completion_provider_changed(&mut self, cx: &mut ModelContext<Self>) { fn completion_provider_changed(&mut self, cx: &mut ModelContext<Self>) {
self.count_remaining_tokens(cx); self.count_remaining_tokens(cx);
} }
@ -2183,6 +2212,7 @@ struct PendingCompletion {
} }
enum ContextEditorEvent { enum ContextEditorEvent {
Edited,
TabContentChanged, TabContentChanged,
} }
@ -2775,6 +2805,7 @@ impl ContextEditor {
EditorEvent::SelectionsChanged { .. } => { EditorEvent::SelectionsChanged { .. } => {
self.scroll_position = self.cursor_scroll_position(cx); self.scroll_position = self.cursor_scroll_position(cx);
} }
EditorEvent::BufferEdited => cx.emit(ContextEditorEvent::Edited),
_ => {} _ => {}
} }
} }

View File

@ -1,8 +1,9 @@
use crate::{ use crate::{
prompts::generate_content_prompt, AssistantPanel, CompletionProvider, Hunk, assistant_settings::AssistantSettings, humanize_token_count, prompts::generate_content_prompt,
LanguageModelRequest, LanguageModelRequestMessage, Role, StreamingDiff, AssistantPanel, AssistantPanelEvent, CompletionProvider, Hunk, LanguageModelRequest,
LanguageModelRequestMessage, Role, StreamingDiff,
}; };
use anyhow::{Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use client::telemetry::Telemetry; use client::telemetry::Telemetry;
use collections::{hash_map, HashMap, HashSet, VecDeque}; use collections::{hash_map, HashMap, HashSet, VecDeque};
use editor::{ use editor::{
@ -14,6 +15,7 @@ use editor::{
Anchor, AnchorRangeExt, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, Anchor, AnchorRangeExt, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle,
ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
}; };
use fs::Fs;
use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
use gpui::{ use gpui::{
point, AppContext, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, Global, point, AppContext, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, Global,
@ -24,7 +26,7 @@ use language::{Buffer, Point, Selection, TransactionId};
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
use parking_lot::Mutex; use parking_lot::Mutex;
use rope::Rope; use rope::Rope;
use settings::Settings; use settings::{update_settings_file, Settings};
use similar::TextDiff; use similar::TextDiff;
use std::{ use std::{
cmp, mem, cmp, mem,
@ -32,15 +34,15 @@ use std::{
pin::Pin, pin::Pin,
sync::Arc, sync::Arc,
task::{self, Poll}, task::{self, Poll},
time::Instant, time::{Duration, Instant},
}; };
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{prelude::*, Tooltip}; use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
use util::RangeExt; use util::RangeExt;
use workspace::{notifications::NotificationId, Toast, Workspace}; use workspace::{notifications::NotificationId, Toast, Workspace};
pub fn init(telemetry: Arc<Telemetry>, cx: &mut AppContext) { pub fn init(fs: Arc<dyn Fs>, telemetry: Arc<Telemetry>, cx: &mut AppContext) {
cx.set_global(InlineAssistant::new(telemetry)); cx.set_global(InlineAssistant::new(fs, telemetry));
} }
const PROMPT_HISTORY_MAX_LEN: usize = 20; const PROMPT_HISTORY_MAX_LEN: usize = 20;
@ -53,12 +55,13 @@ pub struct InlineAssistant {
assist_groups: HashMap<InlineAssistGroupId, InlineAssistGroup>, assist_groups: HashMap<InlineAssistGroupId, InlineAssistGroup>,
prompt_history: VecDeque<String>, prompt_history: VecDeque<String>,
telemetry: Option<Arc<Telemetry>>, telemetry: Option<Arc<Telemetry>>,
fs: Arc<dyn Fs>,
} }
impl Global for InlineAssistant {} impl Global for InlineAssistant {}
impl InlineAssistant { impl InlineAssistant {
pub fn new(telemetry: Arc<Telemetry>) -> Self { pub fn new(fs: Arc<dyn Fs>, telemetry: Arc<Telemetry>) -> Self {
Self { Self {
next_assist_id: InlineAssistId::default(), next_assist_id: InlineAssistId::default(),
next_assist_group_id: InlineAssistGroupId::default(), next_assist_group_id: InlineAssistGroupId::default(),
@ -67,6 +70,7 @@ impl InlineAssistant {
assist_groups: HashMap::default(), assist_groups: HashMap::default(),
prompt_history: VecDeque::default(), prompt_history: VecDeque::default(),
telemetry: Some(telemetry), telemetry: Some(telemetry),
fs,
} }
} }
@ -74,7 +78,7 @@ impl InlineAssistant {
&mut self, &mut self,
editor: &View<Editor>, editor: &View<Editor>,
workspace: Option<WeakView<Workspace>>, workspace: Option<WeakView<Workspace>>,
include_context: bool, assistant_panel: Option<&View<AssistantPanel>>,
cx: &mut WindowContext, cx: &mut WindowContext,
) { ) {
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
@ -151,7 +155,10 @@ impl InlineAssistant {
self.prompt_history.clone(), self.prompt_history.clone(),
prompt_buffer.clone(), prompt_buffer.clone(),
codegen.clone(), codegen.clone(),
editor,
assistant_panel,
workspace.clone(), workspace.clone(),
self.fs.clone(),
cx, cx,
) )
}); });
@ -208,7 +215,7 @@ impl InlineAssistant {
InlineAssist::new( InlineAssist::new(
assist_id, assist_id,
assist_group_id, assist_group_id,
include_context, assistant_panel.is_some(),
editor, editor,
&prompt_editor, &prompt_editor,
block_ids[0], block_ids[0],
@ -706,8 +713,6 @@ impl InlineAssistant {
return; return;
} }
assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
let Some(user_prompt) = assist let Some(user_prompt) = assist
.decorations .decorations
.as_ref() .as_ref()
@ -716,115 +721,138 @@ impl InlineAssistant {
return; return;
}; };
let context = if assist.include_context {
assist.workspace.as_ref().and_then(|workspace| {
let workspace = workspace.upgrade()?.read(cx);
let assistant_panel = workspace.panel::<AssistantPanel>(cx)?;
assistant_panel.read(cx).active_context(cx)
})
} else {
None
};
let editor = if let Some(editor) = assist.editor.upgrade() {
editor
} else {
return;
};
let project_name = assist.workspace.as_ref().and_then(|workspace| {
let workspace = workspace.upgrade()?;
Some(
workspace
.read(cx)
.project()
.read(cx)
.worktree_root_names(cx)
.collect::<Vec<&str>>()
.join("/"),
)
});
self.prompt_history.retain(|prompt| *prompt != user_prompt); self.prompt_history.retain(|prompt| *prompt != user_prompt);
self.prompt_history.push_back(user_prompt.clone()); self.prompt_history.push_back(user_prompt.clone());
if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN { if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN {
self.prompt_history.pop_front(); self.prompt_history.pop_front();
} }
assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
let codegen = assist.codegen.clone(); let codegen = assist.codegen.clone();
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); let request = self.request_for_inline_assist(assist_id, cx);
let range = codegen.read(cx).range.clone();
let start = snapshot.point_to_buffer_offset(range.start);
let end = snapshot.point_to_buffer_offset(range.end);
let (buffer, range) = if let Some((start, end)) = start.zip(end) {
let (start_buffer, start_buffer_offset) = start;
let (end_buffer, end_buffer_offset) = end;
if start_buffer.remote_id() == end_buffer.remote_id() {
(start_buffer.clone(), start_buffer_offset..end_buffer_offset)
} else {
self.finish_assist(assist_id, false, cx);
return;
}
} else {
self.finish_assist(assist_id, false, cx);
return;
};
let language = buffer.language_at(range.start);
let language_name = if let Some(language) = language.as_ref() {
if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
None
} else {
Some(language.name())
}
} else {
None
};
// Higher Temperature increases the randomness of model outputs.
// If Markdown or No Language is Known, increase the randomness for more creative output
// If Code, decrease temperature to get more deterministic outputs
let temperature = if let Some(language) = language_name.clone() {
if language.as_ref() == "Markdown" {
1.0
} else {
0.5
}
} else {
1.0
};
let prompt = cx.background_executor().spawn(async move {
let language_name = language_name.as_deref();
generate_content_prompt(user_prompt, language_name, buffer, range, project_name)
});
let mut messages = Vec::new();
if let Some(context) = context {
let request = context.read(cx).to_completion_request(cx);
messages = request.messages;
}
let model = CompletionProvider::global(cx).model();
cx.spawn(|mut cx| async move { cx.spawn(|mut cx| async move {
let prompt = prompt.await?; let request = request.await?;
codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx))?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn request_for_inline_assist(
&self,
assist_id: InlineAssistId,
cx: &mut WindowContext,
) -> Task<Result<LanguageModelRequest>> {
cx.spawn(|mut cx| async move {
let (user_prompt, context_request, project_name, buffer, range, model) = cx
.read_global(|this: &InlineAssistant, cx: &WindowContext| {
let assist = this.assists.get(&assist_id).context("invalid assist")?;
let decorations = assist.decorations.as_ref().context("invalid assist")?;
let editor = assist.editor.upgrade().context("invalid assist")?;
let user_prompt = decorations.prompt_editor.read(cx).prompt(cx);
let context_request = if assist.include_context {
assist.workspace.as_ref().and_then(|workspace| {
let workspace = workspace.upgrade()?.read(cx);
let assistant_panel = workspace.panel::<AssistantPanel>(cx)?;
Some(
assistant_panel
.read(cx)
.active_context(cx)?
.read(cx)
.to_completion_request(cx),
)
})
} else {
None
};
let project_name = assist.workspace.as_ref().and_then(|workspace| {
let workspace = workspace.upgrade()?;
Some(
workspace
.read(cx)
.project()
.read(cx)
.worktree_root_names(cx)
.collect::<Vec<&str>>()
.join("/"),
)
});
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
let range = assist.codegen.read(cx).range.clone();
let model = CompletionProvider::global(cx).model();
anyhow::Ok((
user_prompt,
context_request,
project_name,
buffer,
range,
model,
))
})??;
let language = buffer.language_at(range.start);
let language_name = if let Some(language) = language.as_ref() {
if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
None
} else {
Some(language.name())
}
} else {
None
};
// Higher Temperature increases the randomness of model outputs.
// If Markdown or No Language is Known, increase the randomness for more creative output
// If Code, decrease temperature to get more deterministic outputs
let temperature = if let Some(language) = language_name.clone() {
if language.as_ref() == "Markdown" {
1.0
} else {
0.5
}
} else {
1.0
};
let prompt = cx
.background_executor()
.spawn(async move {
let language_name = language_name.as_deref();
let start = buffer.point_to_buffer_offset(range.start);
let end = buffer.point_to_buffer_offset(range.end);
let (buffer, range) = if let Some((start, end)) = start.zip(end) {
let (start_buffer, start_buffer_offset) = start;
let (end_buffer, end_buffer_offset) = end;
if start_buffer.remote_id() == end_buffer.remote_id() {
(start_buffer.clone(), start_buffer_offset..end_buffer_offset)
} else {
return Err(anyhow!("invalid transformation range"));
}
} else {
return Err(anyhow!("invalid transformation range"));
};
generate_content_prompt(user_prompt, language_name, buffer, range, project_name)
})
.await?;
let mut messages = Vec::new();
if let Some(context_request) = context_request {
messages = context_request.messages;
}
messages.push(LanguageModelRequestMessage { messages.push(LanguageModelRequestMessage {
role: Role::User, role: Role::User,
content: prompt, content: prompt,
}); });
let request = LanguageModelRequest { Ok(LanguageModelRequest {
model, model,
messages, messages,
stop: vec!["|END|>".to_string()], stop: vec!["|END|>".to_string()],
temperature, temperature,
}; })
codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx))?;
anyhow::Ok(())
}) })
.detach_and_log_err(cx);
} }
fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) { fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
@ -1142,6 +1170,7 @@ enum PromptEditorEvent {
struct PromptEditor { struct PromptEditor {
id: InlineAssistId, id: InlineAssistId,
fs: Arc<dyn Fs>,
height_in_lines: u8, height_in_lines: u8,
editor: View<Editor>, editor: View<Editor>,
edited_since_done: bool, edited_since_done: bool,
@ -1150,9 +1179,12 @@ struct PromptEditor {
prompt_history_ix: Option<usize>, prompt_history_ix: Option<usize>,
pending_prompt: String, pending_prompt: String,
codegen: Model<Codegen>, codegen: Model<Codegen>,
workspace: Option<WeakView<Workspace>>,
_codegen_subscription: Subscription, _codegen_subscription: Subscription,
editor_subscriptions: Vec<Subscription>, editor_subscriptions: Vec<Subscription>,
pending_token_count: Task<Result<()>>,
token_count: Option<usize>,
_token_count_subscriptions: Vec<Subscription>,
workspace: Option<WeakView<Workspace>>,
} }
impl EventEmitter<PromptEditorEvent> for PromptEditor {} impl EventEmitter<PromptEditorEvent> for PromptEditor {}
@ -1160,6 +1192,7 @@ impl EventEmitter<PromptEditorEvent> for PromptEditor {}
impl Render for PromptEditor { impl Render for PromptEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let gutter_dimensions = *self.gutter_dimensions.lock(); let gutter_dimensions = *self.gutter_dimensions.lock();
let fs = self.fs.clone();
let buttons = match &self.codegen.read(cx).status { let buttons = match &self.codegen.read(cx).status {
CodegenStatus::Idle => { CodegenStatus::Idle => {
@ -1245,85 +1278,100 @@ impl Render for PromptEditor {
} }
}; };
v_flex().h_full().w_full().justify_end().child( h_flex()
h_flex() .bg(cx.theme().colors().editor_background)
.bg(cx.theme().colors().editor_background) .border_y_1()
.border_y_1() .border_color(cx.theme().status().info_border)
.border_color(cx.theme().status().info_border) .py_1p5()
.py_1p5() .h_full()
.w_full() .w_full()
.on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up)) .on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down)) .on_action(cx.listener(Self::move_down))
.child( .child(
h_flex() h_flex()
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0)) .w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
// .pr(gutter_dimensions.fold_area_width()) .justify_center()
.justify_center() .gap_2()
.gap_2() .child(
.children(self.workspace.clone().map(|workspace| { PopoverMenu::new("model-switcher")
IconButton::new("context", IconName::Context) .menu(move |cx| {
.size(ButtonSize::None) ContextMenu::build(cx, |mut menu, cx| {
.icon_size(IconSize::XSmall) for model in CompletionProvider::global(cx).available_models() {
.icon_color(Color::Muted) menu = menu.custom_entry(
.on_click({ {
let workspace = workspace.clone(); let model = model.clone();
cx.listener(move |_, _, cx| { move |_| {
workspace Label::new(model.display_name())
.update(cx, |workspace, cx| { .into_any_element()
workspace.focus_panel::<AssistantPanel>(cx); }
}) },
.ok(); {
}) let fs = fs.clone();
let model = model.clone();
move |cx| {
let model = model.clone();
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings| settings.set_model(model),
);
}
},
);
}
menu
}) })
.tooltip(move |cx| { .into()
let token_count = workspace.upgrade().and_then(|workspace| { })
let panel = .trigger(
workspace.read(cx).panel::<AssistantPanel>(cx)?; IconButton::new("context", IconName::Settings)
let context = panel.read(cx).active_context(cx)?; .size(ButtonSize::None)
context.read(cx).token_count() .icon_size(IconSize::Small)
}); .icon_color(Color::Muted)
if let Some(token_count) = token_count { .tooltip(move |cx| {
Tooltip::with_meta( Tooltip::with_meta(
format!( format!(
"{} Additional Context Tokens from Assistant", "Using {}",
token_count CompletionProvider::global(cx)
.model()
.display_name()
), ),
Some(&crate::ToggleFocus), None,
"Click to open…", "Click to Change Model",
cx, cx,
) )
} else { }),
Tooltip::for_action( )
"Toggle Assistant Panel", .anchor(gpui::AnchorCorner::BottomRight),
&crate::ToggleFocus, )
cx, .children(
) if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
} let error_message = SharedString::from(error.to_string());
}) Some(
})) div()
.children( .id("error")
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status { .tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
let error_message = SharedString::from(error.to_string()); .child(
Some( Icon::new(IconName::XCircle)
div() .size(IconSize::Small)
.id("error") .color(Color::Error),
.tooltip(move |cx| Tooltip::text(error_message.clone(), cx)) ),
.child( )
Icon::new(IconName::XCircle) } else {
.size(IconSize::Small) None
.color(Color::Error), },
), ),
) )
} else { .child(div().flex_1().child(self.render_prompt_editor(cx)))
None .child(
}, h_flex()
), .gap_2()
) .pr_4()
.child(div().flex_1().child(self.render_prompt_editor(cx))) .children(self.render_token_count(cx))
.child(h_flex().gap_2().pr_4().children(buttons)), .children(buttons),
) )
} }
} }
@ -1336,13 +1384,17 @@ impl FocusableView for PromptEditor {
impl PromptEditor { impl PromptEditor {
const MAX_LINES: u8 = 8; const MAX_LINES: u8 = 8;
#[allow(clippy::too_many_arguments)]
fn new( fn new(
id: InlineAssistId, id: InlineAssistId,
gutter_dimensions: Arc<Mutex<GutterDimensions>>, gutter_dimensions: Arc<Mutex<GutterDimensions>>,
prompt_history: VecDeque<String>, prompt_history: VecDeque<String>,
prompt_buffer: Model<MultiBuffer>, prompt_buffer: Model<MultiBuffer>,
codegen: Model<Codegen>, codegen: Model<Codegen>,
parent_editor: &View<Editor>,
assistant_panel: Option<&View<AssistantPanel>>,
workspace: Option<WeakView<Workspace>>, workspace: Option<WeakView<Workspace>>,
fs: Arc<dyn Fs>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
let prompt_editor = cx.new_view(|cx| { let prompt_editor = cx.new_view(|cx| {
@ -1363,6 +1415,15 @@ impl PromptEditor {
editor.set_placeholder_text("Add a prompt…", cx); editor.set_placeholder_text("Add a prompt…", cx);
editor editor
}); });
let mut token_count_subscriptions = Vec::new();
token_count_subscriptions
.push(cx.subscribe(parent_editor, Self::handle_parent_editor_event));
if let Some(assistant_panel) = assistant_panel {
token_count_subscriptions
.push(cx.subscribe(assistant_panel, Self::handle_assistant_panel_event));
}
let mut this = Self { let mut this = Self {
id, id,
height_in_lines: 1, height_in_lines: 1,
@ -1375,9 +1436,14 @@ impl PromptEditor {
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed), _codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
editor_subscriptions: Vec::new(), editor_subscriptions: Vec::new(),
codegen, codegen,
fs,
pending_token_count: Task::ready(Ok(())),
token_count: None,
_token_count_subscriptions: token_count_subscriptions,
workspace, workspace,
}; };
this.count_lines(cx); this.count_lines(cx);
this.count_tokens(cx);
this.subscribe_to_editor(cx); this.subscribe_to_editor(cx);
this this
} }
@ -1436,6 +1502,47 @@ impl PromptEditor {
} }
} }
fn handle_parent_editor_event(
&mut self,
_: View<Editor>,
event: &EditorEvent,
cx: &mut ViewContext<Self>,
) {
if let EditorEvent::BufferEdited { .. } = event {
self.count_tokens(cx);
}
}
fn handle_assistant_panel_event(
&mut self,
_: View<AssistantPanel>,
event: &AssistantPanelEvent,
cx: &mut ViewContext<Self>,
) {
let AssistantPanelEvent::ContextEdited { .. } = event;
self.count_tokens(cx);
}
fn count_tokens(&mut self, cx: &mut ViewContext<Self>) {
let assist_id = self.id;
self.pending_token_count = cx.spawn(|this, mut cx| async move {
cx.background_executor().timer(Duration::from_secs(1)).await;
let request = cx
.update_global(|inline_assistant: &mut InlineAssistant, cx| {
inline_assistant.request_for_inline_assist(assist_id, cx)
})?
.await?;
let token_count = cx
.update(|cx| CompletionProvider::global(cx).count_tokens(request, cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.token_count = Some(token_count);
cx.notify();
})
})
}
fn handle_prompt_editor_changed(&mut self, _: View<Editor>, cx: &mut ViewContext<Self>) { fn handle_prompt_editor_changed(&mut self, _: View<Editor>, cx: &mut ViewContext<Self>) {
self.count_lines(cx); self.count_lines(cx);
} }
@ -1460,6 +1567,9 @@ impl PromptEditor {
self.edited_since_done = true; self.edited_since_done = true;
cx.notify(); cx.notify();
} }
EditorEvent::BufferEdited => {
self.count_tokens(cx);
}
_ => {} _ => {}
} }
} }
@ -1551,6 +1661,63 @@ impl PromptEditor {
} }
} }
fn render_token_count(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
let model = CompletionProvider::global(cx).model();
let token_count = self.token_count?;
let max_token_count = model.max_token_count();
let remaining_tokens = max_token_count as isize - token_count as isize;
let token_count_color = if remaining_tokens <= 0 {
Color::Error
} else if token_count as f32 / max_token_count as f32 >= 0.8 {
Color::Warning
} else {
Color::Muted
};
let mut token_count = h_flex()
.id("token_count")
.gap_0p5()
.child(
Label::new(humanize_token_count(token_count))
.size(LabelSize::Small)
.color(token_count_color),
)
.child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
.child(
Label::new(humanize_token_count(max_token_count))
.size(LabelSize::Small)
.color(Color::Muted),
);
if let Some(workspace) = self.workspace.clone() {
token_count = token_count
.tooltip(|cx| {
Tooltip::with_meta(
"Tokens Used by Inline Assistant",
None,
"Click to Open Assistant Panel",
cx,
)
})
.cursor_pointer()
.on_mouse_down(gpui::MouseButton::Left, |_, cx| cx.stop_propagation())
.on_click(move |_, cx| {
cx.stop_propagation();
workspace
.update(cx, |workspace, cx| {
workspace.focus_panel::<AssistantPanel>(cx)
})
.ok();
});
} else {
token_count = token_count
.cursor_default()
.tooltip(|cx| Tooltip::text("Tokens Used by Inline Assistant", cx));
}
Some(token_count)
}
fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx); let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle { let text_style = TextStyle {

View File

@ -569,7 +569,7 @@ impl PromptLibrary {
let provider = CompletionProvider::global(cx); let provider = CompletionProvider::global(cx);
if provider.is_authenticated() { if provider.is_authenticated() {
InlineAssistant::update_global(cx, |assistant, cx| { InlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&prompt_editor, None, false, cx) assistant.assist(&prompt_editor, None, None, cx)
}) })
} else { } else {
for window in cx.windows() { for window in cx.windows() {

View File

@ -219,7 +219,7 @@ fn init_ui(app_state: Arc<AppState>, cx: &mut AppContext) -> Result<()> {
inline_completion_registry::init(app_state.client.telemetry().clone(), cx); inline_completion_registry::init(app_state.client.telemetry().clone(), cx);
assistant::init(app_state.client.clone(), cx); assistant::init(app_state.fs.clone(), app_state.client.clone(), cx);
repl::init(app_state.fs.clone(), cx); repl::init(app_state.fs.clone(), cx);

View File

@ -3181,7 +3181,7 @@ mod tests {
project_panel::init((), cx); project_panel::init((), cx);
outline_panel::init((), cx); outline_panel::init((), cx);
terminal_view::init(cx); terminal_view::init(cx);
assistant::init(app_state.client.clone(), cx); assistant::init(app_state.fs.clone(), app_state.client.clone(), cx);
tasks_ui::init(cx); tasks_ui::init(cx);
initialize_workspace(app_state.clone(), cx); initialize_workspace(app_state.clone(), cx);
app_state app_state