use crate::assistant_settings::AssistantSettings; use crate::{ humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent, RequestType, DEFAULT_CONTEXT_LINES, }; use anyhow::{Context as _, Result}; use client::telemetry::Telemetry; use collections::{HashMap, VecDeque}; use editor::{ actions::{MoveDown, MoveUp, SelectAll}, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer, }; use fs::Fs; use futures::{channel::mpsc, SinkExt, StreamExt}; use gpui::{ AppContext, Context, EventEmitter, FocusHandle, FocusableView, Global, Model, ModelContext, Subscription, Task, TextStyle, UpdateGlobal, View, WeakView, }; use language::Buffer; use language_model::{ LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, }; use language_model_selector::LanguageModelSelector; use language_models::report_assistant_event; use settings::{update_settings_file, Settings}; use std::{ cmp, sync::Arc, time::{Duration, Instant}, }; use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase}; use terminal::Terminal; use terminal_view::TerminalView; use theme::ThemeSettings; use ui::{prelude::*, IconButtonShape, Tooltip}; use util::ResultExt; use workspace::{notifications::NotificationId, Toast, Workspace}; pub fn init( fs: Arc, prompt_builder: Arc, telemetry: Arc, cx: &mut AppContext, ) { cx.set_global(TerminalInlineAssistant::new(fs, prompt_builder, telemetry)); } const PROMPT_HISTORY_MAX_LEN: usize = 20; #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)] struct TerminalInlineAssistId(usize); impl TerminalInlineAssistId { fn post_inc(&mut self) -> TerminalInlineAssistId { let id = *self; self.0 += 1; id } } pub struct TerminalInlineAssistant { next_assist_id: TerminalInlineAssistId, assists: HashMap, prompt_history: VecDeque, telemetry: Option>, fs: Arc, prompt_builder: Arc, } impl Global for TerminalInlineAssistant {} impl TerminalInlineAssistant { pub fn new( fs: Arc, prompt_builder: Arc, telemetry: Arc, ) -> Self { Self { next_assist_id: TerminalInlineAssistId::default(), assists: HashMap::default(), prompt_history: VecDeque::default(), telemetry: Some(telemetry), fs, prompt_builder, } } pub fn assist( &mut self, terminal_view: &View, workspace: Option>, assistant_panel: Option<&View>, initial_prompt: Option, cx: &mut WindowContext, ) { let terminal = terminal_view.read(cx).terminal().clone(); let assist_id = self.next_assist_id.post_inc(); let prompt_buffer = cx.new_model(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx)); let prompt_buffer = cx.new_model(|cx| MultiBuffer::singleton(prompt_buffer, cx)); let codegen = cx.new_model(|_| Codegen::new(terminal, self.telemetry.clone())); let prompt_editor = cx.new_view(|cx| { PromptEditor::new( assist_id, self.prompt_history.clone(), prompt_buffer.clone(), codegen, assistant_panel, workspace.clone(), self.fs.clone(), cx, ) }); let prompt_editor_render = prompt_editor.clone(); let block = terminal_view::BlockProperties { height: 2, render: Box::new(move |_| prompt_editor_render.clone().into_any_element()), }; terminal_view.update(cx, |terminal_view, cx| { terminal_view.set_block_below_cursor(block, cx); }); let terminal_assistant = TerminalInlineAssist::new( assist_id, terminal_view, assistant_panel.is_some(), prompt_editor, workspace.clone(), cx, ); self.assists.insert(assist_id, terminal_assistant); self.focus_assist(assist_id, cx); } fn focus_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) { let assist = &self.assists[&assist_id]; if let Some(prompt_editor) = assist.prompt_editor.as_ref() { prompt_editor.update(cx, |this, cx| { this.editor.update(cx, |editor, cx| { editor.focus(cx); editor.select_all(&SelectAll, cx); }); }); } } fn handle_prompt_editor_event( &mut self, prompt_editor: View, event: &PromptEditorEvent, cx: &mut WindowContext, ) { let assist_id = prompt_editor.read(cx).id; match event { PromptEditorEvent::StartRequested => { self.start_assist(assist_id, cx); } PromptEditorEvent::StopRequested => { self.stop_assist(assist_id, cx); } PromptEditorEvent::ConfirmRequested { execute } => { self.finish_assist(assist_id, false, *execute, cx); } PromptEditorEvent::CancelRequested => { self.finish_assist(assist_id, true, false, cx); } PromptEditorEvent::DismissRequested => { self.dismiss_assist(assist_id, cx); } PromptEditorEvent::Resized { height_in_lines } => { self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, cx); } } } fn start_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) { let assist = if let Some(assist) = self.assists.get_mut(&assist_id) { assist } else { return; }; let Some(user_prompt) = assist .prompt_editor .as_ref() .map(|editor| editor.read(cx).prompt(cx)) else { return; }; self.prompt_history.retain(|prompt| *prompt != user_prompt); self.prompt_history.push_back(user_prompt.clone()); if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN { self.prompt_history.pop_front(); } assist .terminal .update(cx, |terminal, cx| { terminal .terminal() .update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string())); }) .log_err(); let codegen = assist.codegen.clone(); let Some(request) = self.request_for_inline_assist(assist_id, cx).log_err() else { return; }; codegen.update(cx, |codegen, cx| codegen.start(request, cx)); } fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) { let assist = if let Some(assist) = self.assists.get_mut(&assist_id) { assist } else { return; }; assist.codegen.update(cx, |codegen, cx| codegen.stop(cx)); } fn request_for_inline_assist( &self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext, ) -> Result { let assist = self.assists.get(&assist_id).context("invalid assist")?; let shell = std::env::var("SHELL").ok(); let (latest_output, working_directory) = assist .terminal .update(cx, |terminal, cx| { let terminal = terminal.model().read(cx); let latest_output = terminal.last_n_non_empty_lines(DEFAULT_CONTEXT_LINES); let working_directory = terminal .working_directory() .map(|path| path.to_string_lossy().to_string()); (latest_output, working_directory) }) .ok() .unwrap_or_default(); 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::(cx)?; Some( assistant_panel .read(cx) .active_context(cx)? .read(cx) .to_completion_request(RequestType::Chat, cx), ) }) } else { None }; let prompt = self.prompt_builder.generate_terminal_assistant_prompt( &assist .prompt_editor .clone() .context("invalid assist")? .read(cx) .prompt(cx), shell.as_deref(), working_directory.as_deref(), &latest_output, )?; let mut messages = Vec::new(); if let Some(context_request) = context_request { messages = context_request.messages; } messages.push(LanguageModelRequestMessage { role: Role::User, content: vec![prompt.into()], cache: false, }); Ok(LanguageModelRequest { messages, tools: Vec::new(), stop: Vec::new(), temperature: None, }) } fn finish_assist( &mut self, assist_id: TerminalInlineAssistId, undo: bool, execute: bool, cx: &mut WindowContext, ) { self.dismiss_assist(assist_id, cx); if let Some(assist) = self.assists.remove(&assist_id) { assist .terminal .update(cx, |this, cx| { this.clear_block_below_cursor(cx); this.focus_handle(cx).focus(cx); }) .log_err(); if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { let codegen = assist.codegen.read(cx); let executor = cx.background_executor().clone(); report_assistant_event( AssistantEvent { conversation_id: None, kind: AssistantKind::InlineTerminal, message_id: codegen.message_id.clone(), phase: if undo { AssistantPhase::Rejected } else { AssistantPhase::Accepted }, model: model.telemetry_id(), model_provider: model.provider_id().to_string(), response_latency: None, error_message: None, language_name: None, }, codegen.telemetry.clone(), cx.http_client(), model.api_key(cx), &executor, ); } assist.codegen.update(cx, |codegen, cx| { if undo { codegen.undo(cx); } else if execute { codegen.complete(cx); } }); } } fn dismiss_assist( &mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext, ) -> bool { let Some(assist) = self.assists.get_mut(&assist_id) else { return false; }; if assist.prompt_editor.is_none() { return false; } assist.prompt_editor = None; assist .terminal .update(cx, |this, cx| { this.clear_block_below_cursor(cx); this.focus_handle(cx).focus(cx); }) .is_ok() } fn insert_prompt_editor_into_terminal( &mut self, assist_id: TerminalInlineAssistId, height: u8, cx: &mut WindowContext, ) { if let Some(assist) = self.assists.get_mut(&assist_id) { if let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned() { assist .terminal .update(cx, |terminal, cx| { terminal.clear_block_below_cursor(cx); let block = terminal_view::BlockProperties { height, render: Box::new(move |_| prompt_editor.clone().into_any_element()), }; terminal.set_block_below_cursor(block, cx); }) .log_err(); } } } } struct TerminalInlineAssist { terminal: WeakView, prompt_editor: Option>, codegen: Model, workspace: Option>, include_context: bool, _subscriptions: Vec, } impl TerminalInlineAssist { pub fn new( assist_id: TerminalInlineAssistId, terminal: &View, include_context: bool, prompt_editor: View, workspace: Option>, cx: &mut WindowContext, ) -> Self { let codegen = prompt_editor.read(cx).codegen.clone(); Self { terminal: terminal.downgrade(), prompt_editor: Some(prompt_editor.clone()), codegen: codegen.clone(), workspace: workspace.clone(), include_context, _subscriptions: vec![ cx.subscribe(&prompt_editor, |prompt_editor, event, cx| { TerminalInlineAssistant::update_global(cx, |this, cx| { this.handle_prompt_editor_event(prompt_editor, event, cx) }) }), cx.subscribe(&codegen, move |codegen, event, cx| { TerminalInlineAssistant::update_global(cx, |this, cx| match event { CodegenEvent::Finished => { let assist = if let Some(assist) = this.assists.get(&assist_id) { assist } else { return; }; if let CodegenStatus::Error(error) = &codegen.read(cx).status { if assist.prompt_editor.is_none() { if let Some(workspace) = assist .workspace .as_ref() .and_then(|workspace| workspace.upgrade()) { let error = format!("Terminal inline assistant error: {}", error); workspace.update(cx, |workspace, cx| { struct InlineAssistantError; let id = NotificationId::composite::( assist_id.0, ); workspace.show_toast(Toast::new(id, error), cx); }) } } } if assist.prompt_editor.is_none() { this.finish_assist(assist_id, false, false, cx); } } }) }), ], } } } enum PromptEditorEvent { StartRequested, StopRequested, ConfirmRequested { execute: bool }, CancelRequested, DismissRequested, Resized { height_in_lines: u8 }, } struct PromptEditor { id: TerminalInlineAssistId, fs: Arc, height_in_lines: u8, editor: View, edited_since_done: bool, prompt_history: VecDeque, prompt_history_ix: Option, pending_prompt: String, codegen: Model, _codegen_subscription: Subscription, editor_subscriptions: Vec, pending_token_count: Task>, token_count: Option, _token_count_subscriptions: Vec, workspace: Option>, } impl EventEmitter for PromptEditor {} impl Render for PromptEditor { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let status = &self.codegen.read(cx).status; let buttons = match status { CodegenStatus::Idle => { vec![ IconButton::new("cancel", IconName::Close) .icon_color(Color::Muted) .shape(IconButtonShape::Square) .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx)) .on_click( cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)), ), IconButton::new("start", IconName::SparkleAlt) .icon_color(Color::Muted) .shape(IconButtonShape::Square) .tooltip(|cx| Tooltip::for_action("Generate", &menu::Confirm, cx)) .on_click( cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)), ), ] } CodegenStatus::Pending => { vec![ IconButton::new("cancel", IconName::Close) .icon_color(Color::Muted) .shape(IconButtonShape::Square) .tooltip(|cx| Tooltip::text("Cancel Assist", cx)) .on_click( cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)), ), IconButton::new("stop", IconName::Stop) .icon_color(Color::Error) .shape(IconButtonShape::Square) .tooltip(|cx| { Tooltip::with_meta( "Interrupt Generation", Some(&menu::Cancel), "Changes won't be discarded", cx, ) }) .on_click( cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)), ), ] } CodegenStatus::Error(_) | CodegenStatus::Done => { let cancel = IconButton::new("cancel", IconName::Close) .icon_color(Color::Muted) .shape(IconButtonShape::Square) .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx)) .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested))); let has_error = matches!(status, CodegenStatus::Error(_)); if has_error || self.edited_since_done { vec![ cancel, IconButton::new("restart", IconName::RotateCw) .icon_color(Color::Info) .shape(IconButtonShape::Square) .tooltip(|cx| { Tooltip::with_meta( "Restart Generation", Some(&menu::Confirm), "Changes will be discarded", cx, ) }) .on_click(cx.listener(|_, _, cx| { cx.emit(PromptEditorEvent::StartRequested); })), ] } else { vec![ cancel, IconButton::new("accept", IconName::Check) .icon_color(Color::Info) .shape(IconButtonShape::Square) .tooltip(|cx| { Tooltip::for_action("Accept Generated Command", &menu::Confirm, cx) }) .on_click(cx.listener(|_, _, cx| { cx.emit(PromptEditorEvent::ConfirmRequested { execute: false }); })), IconButton::new("confirm", IconName::Play) .icon_color(Color::Info) .shape(IconButtonShape::Square) .tooltip(|cx| { Tooltip::for_action( "Execute Generated Command", &menu::SecondaryConfirm, cx, ) }) .on_click(cx.listener(|_, _, cx| { cx.emit(PromptEditorEvent::ConfirmRequested { execute: true }); })), ] } } }; h_flex() .bg(cx.theme().colors().editor_background) .border_y_1() .border_color(cx.theme().status().info_border) .py_2() .h_full() .w_full() .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::secondary_confirm)) .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::move_up)) .on_action(cx.listener(Self::move_down)) .child( h_flex() .w_12() .justify_center() .gap_2() .child(LanguageModelSelector::new( { let fs = self.fs.clone(); move |model, cx| { update_settings_file::( fs.clone(), cx, move |settings, _| settings.set_model(model.clone()), ); } }, IconButton::new("context", IconName::SettingsAlt) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .icon_color(Color::Muted) .tooltip(move |cx| { Tooltip::with_meta( format!( "Using {}", LanguageModelRegistry::read_global(cx) .active_model() .map(|model| model.name().0) .unwrap_or_else(|| "No model selected".into()), ), None, "Change Model", cx, ) }), )) .children( if let CodegenStatus::Error(error) = &self.codegen.read(cx).status { let error_message = SharedString::from(error.to_string()); Some( div() .id("error") .tooltip(move |cx| Tooltip::text(error_message.clone(), cx)) .child( Icon::new(IconName::XCircle) .size(IconSize::Small) .color(Color::Error), ), ) } else { None }, ), ) .child(div().flex_1().child(self.render_prompt_editor(cx))) .child( h_flex() .gap_1() .pr_4() .children(self.render_token_count(cx)) .children(buttons), ) } } impl FocusableView for PromptEditor { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { self.editor.focus_handle(cx) } } impl PromptEditor { const MAX_LINES: u8 = 8; #[allow(clippy::too_many_arguments)] fn new( id: TerminalInlineAssistId, prompt_history: VecDeque, prompt_buffer: Model, codegen: Model, assistant_panel: Option<&View>, workspace: Option>, fs: Arc, cx: &mut ViewContext, ) -> Self { let prompt_editor = cx.new_view(|cx| { let mut editor = Editor::new( EditorMode::AutoHeight { max_lines: Self::MAX_LINES as usize, }, prompt_buffer, None, false, cx, ); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); editor.set_placeholder_text("Add a prompt…", cx); editor }); let mut token_count_subscriptions = Vec::new(); if let Some(assistant_panel) = assistant_panel { token_count_subscriptions .push(cx.subscribe(assistant_panel, Self::handle_assistant_panel_event)); } let mut this = Self { id, height_in_lines: 1, editor: prompt_editor, edited_since_done: false, prompt_history, prompt_history_ix: None, pending_prompt: String::new(), _codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed), editor_subscriptions: Vec::new(), codegen, fs, pending_token_count: Task::ready(Ok(())), token_count: None, _token_count_subscriptions: token_count_subscriptions, workspace, }; this.count_lines(cx); this.count_tokens(cx); this.subscribe_to_editor(cx); this } fn subscribe_to_editor(&mut self, cx: &mut ViewContext) { self.editor_subscriptions.clear(); self.editor_subscriptions .push(cx.observe(&self.editor, Self::handle_prompt_editor_changed)); self.editor_subscriptions .push(cx.subscribe(&self.editor, Self::handle_prompt_editor_events)); } fn prompt(&self, cx: &AppContext) -> String { self.editor.read(cx).text(cx) } fn count_lines(&mut self, cx: &mut ViewContext) { let height_in_lines = cmp::max( 2, // Make the editor at least two lines tall, to account for padding and buttons. cmp::min( self.editor .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1), Self::MAX_LINES as u32, ), ) as u8; if height_in_lines != self.height_in_lines { self.height_in_lines = height_in_lines; cx.emit(PromptEditorEvent::Resized { height_in_lines }); } } fn handle_assistant_panel_event( &mut self, _: View, event: &AssistantPanelEvent, cx: &mut ViewContext, ) { let AssistantPanelEvent::ContextEdited { .. } = event; self.count_tokens(cx); } fn count_tokens(&mut self, cx: &mut ViewContext) { let assist_id = self.id; let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else { return; }; 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 TerminalInlineAssistant, cx| { inline_assistant.request_for_inline_assist(assist_id, cx) })??; let token_count = cx.update(|cx| model.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, cx: &mut ViewContext) { self.count_lines(cx); } fn handle_prompt_editor_events( &mut self, _: View, event: &EditorEvent, cx: &mut ViewContext, ) { match event { EditorEvent::Edited { .. } => { let prompt = self.editor.read(cx).text(cx); if self .prompt_history_ix .map_or(true, |ix| self.prompt_history[ix] != prompt) { self.prompt_history_ix.take(); self.pending_prompt = prompt; } self.edited_since_done = true; cx.notify(); } EditorEvent::BufferEdited => { self.count_tokens(cx); } _ => {} } } fn handle_codegen_changed(&mut self, _: Model, cx: &mut ViewContext) { match &self.codegen.read(cx).status { CodegenStatus::Idle => { self.editor .update(cx, |editor, _| editor.set_read_only(false)); } CodegenStatus::Pending => { self.editor .update(cx, |editor, _| editor.set_read_only(true)); } CodegenStatus::Done | CodegenStatus::Error(_) => { self.edited_since_done = false; self.editor .update(cx, |editor, _| editor.set_read_only(false)); } } } fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext) { match &self.codegen.read(cx).status { CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => { cx.emit(PromptEditorEvent::CancelRequested); } CodegenStatus::Pending => { cx.emit(PromptEditorEvent::StopRequested); } } } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { match &self.codegen.read(cx).status { CodegenStatus::Idle => { if !self.editor.read(cx).text(cx).trim().is_empty() { cx.emit(PromptEditorEvent::StartRequested); } } CodegenStatus::Pending => { cx.emit(PromptEditorEvent::DismissRequested); } CodegenStatus::Done => { if self.edited_since_done { cx.emit(PromptEditorEvent::StartRequested); } else { cx.emit(PromptEditorEvent::ConfirmRequested { execute: false }); } } CodegenStatus::Error(_) => { cx.emit(PromptEditorEvent::StartRequested); } } } fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { if matches!(self.codegen.read(cx).status, CodegenStatus::Done) { cx.emit(PromptEditorEvent::ConfirmRequested { execute: true }); } } fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { if let Some(ix) = self.prompt_history_ix { if ix > 0 { self.prompt_history_ix = Some(ix - 1); let prompt = self.prompt_history[ix - 1].as_str(); self.editor.update(cx, |editor, cx| { editor.set_text(prompt, cx); editor.move_to_beginning(&Default::default(), cx); }); } } else if !self.prompt_history.is_empty() { self.prompt_history_ix = Some(self.prompt_history.len() - 1); let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str(); self.editor.update(cx, |editor, cx| { editor.set_text(prompt, cx); editor.move_to_beginning(&Default::default(), cx); }); } } fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { if let Some(ix) = self.prompt_history_ix { if ix < self.prompt_history.len() - 1 { self.prompt_history_ix = Some(ix + 1); let prompt = self.prompt_history[ix + 1].as_str(); self.editor.update(cx, |editor, cx| { editor.set_text(prompt, cx); editor.move_to_end(&Default::default(), cx) }); } else { self.prompt_history_ix = None; let prompt = self.pending_prompt.as_str(); self.editor.update(cx, |editor, cx| { editor.set_text(prompt, cx); editor.move_to_end(&Default::default(), cx) }); } } } fn render_token_count(&self, cx: &mut ViewContext) -> Option { let model = LanguageModelRegistry::read_global(cx).active_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::(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) -> impl IntoElement { let settings = ThemeSettings::get_global(cx); let text_style = TextStyle { color: if self.editor.read(cx).read_only(cx) { cx.theme().colors().text_disabled } else { cx.theme().colors().text }, font_family: settings.buffer_font.family.clone(), font_fallbacks: settings.buffer_font.fallbacks.clone(), font_size: settings.buffer_font_size.into(), font_weight: settings.buffer_font.weight, line_height: relative(settings.buffer_line_height.value()), ..Default::default() }; EditorElement::new( &self.editor, EditorStyle { background: cx.theme().colors().editor_background, local_player: cx.theme().players().local(), text: text_style, ..Default::default() }, ) } } #[derive(Debug)] pub enum CodegenEvent { Finished, } impl EventEmitter for Codegen {} const CLEAR_INPUT: &str = "\x15"; const CARRIAGE_RETURN: &str = "\x0d"; struct TerminalTransaction { terminal: Model, } impl TerminalTransaction { pub fn start(terminal: Model) -> Self { Self { terminal } } pub fn push(&mut self, hunk: String, cx: &mut AppContext) { // Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal let input = Self::sanitize_input(hunk); self.terminal .update(cx, |terminal, _| terminal.input(input)); } pub fn undo(&self, cx: &mut AppContext) { self.terminal .update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string())); } pub fn complete(&self, cx: &mut AppContext) { self.terminal.update(cx, |terminal, _| { terminal.input(CARRIAGE_RETURN.to_string()) }); } fn sanitize_input(input: String) -> String { input.replace(['\r', '\n'], "") } } pub struct Codegen { status: CodegenStatus, telemetry: Option>, terminal: Model, generation: Task<()>, message_id: Option, transaction: Option, } impl Codegen { pub fn new(terminal: Model, telemetry: Option>) -> Self { Self { terminal, telemetry, status: CodegenStatus::Idle, generation: Task::ready(()), message_id: None, transaction: None, } } pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext) { let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else { return; }; let model_api_key = model.api_key(cx); let http_client = cx.http_client(); let telemetry = self.telemetry.clone(); self.status = CodegenStatus::Pending; self.transaction = Some(TerminalTransaction::start(self.terminal.clone())); self.generation = cx.spawn(|this, mut cx| async move { let model_telemetry_id = model.telemetry_id(); let model_provider_id = model.provider_id(); let response = model.stream_completion_text(prompt, &cx).await; let generate = async { let message_id = response .as_ref() .ok() .and_then(|response| response.message_id.clone()); let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); let task = cx.background_executor().spawn({ let message_id = message_id.clone(); let executor = cx.background_executor().clone(); async move { let mut response_latency = None; let request_start = Instant::now(); let task = async { let mut chunks = response?.stream; while let Some(chunk) = chunks.next().await { if response_latency.is_none() { response_latency = Some(request_start.elapsed()); } let chunk = chunk?; hunks_tx.send(chunk).await?; } anyhow::Ok(()) }; let result = task.await; let error_message = result.as_ref().err().map(|error| error.to_string()); report_assistant_event( AssistantEvent { conversation_id: None, kind: AssistantKind::InlineTerminal, message_id, phase: AssistantPhase::Response, model: model_telemetry_id, model_provider: model_provider_id.to_string(), response_latency, error_message, language_name: None, }, telemetry, http_client, model_api_key, &executor, ); result?; anyhow::Ok(()) } }); this.update(&mut cx, |this, _| { this.message_id = message_id; })?; while let Some(hunk) = hunks_rx.next().await { this.update(&mut cx, |this, cx| { if let Some(transaction) = &mut this.transaction { transaction.push(hunk, cx); cx.notify(); } })?; } task.await?; anyhow::Ok(()) }; let result = generate.await; this.update(&mut cx, |this, cx| { if let Err(error) = result { this.status = CodegenStatus::Error(error); } else { this.status = CodegenStatus::Done; } cx.emit(CodegenEvent::Finished); cx.notify(); }) .ok(); }); cx.notify(); } pub fn stop(&mut self, cx: &mut ModelContext) { self.status = CodegenStatus::Done; self.generation = Task::ready(()); cx.emit(CodegenEvent::Finished); cx.notify(); } pub fn complete(&mut self, cx: &mut ModelContext) { if let Some(transaction) = self.transaction.take() { transaction.complete(cx); } } pub fn undo(&mut self, cx: &mut ModelContext) { if let Some(transaction) = self.transaction.take() { transaction.undo(cx); } } } enum CodegenStatus { Idle, Pending, Done, Error(anyhow::Error), }