1use crate::{
  2    context::load_context,
  3    context_store::ContextStore,
  4    inline_prompt_editor::{
  5        CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
  6    },
  7    terminal_codegen::{CLEAR_INPUT, CodegenEvent, TerminalCodegen},
  8};
  9use agent::HistoryStore;
 10use agent_settings::AgentSettings;
 11use anyhow::{Context as _, Result};
 12use client::telemetry::Telemetry;
 13use cloud_llm_client::CompletionIntent;
 14use collections::{HashMap, VecDeque};
 15use editor::{MultiBuffer, actions::SelectAll};
 16use fs::Fs;
 17use gpui::{App, Entity, Focusable, Global, Subscription, Task, UpdateGlobal, WeakEntity};
 18use language::Buffer;
 19use language_model::{
 20    ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
 21    Role, report_assistant_event,
 22};
 23use project::Project;
 24use prompt_store::{PromptBuilder, PromptStore};
 25use std::sync::Arc;
 26use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
 27use terminal_view::TerminalView;
 28use ui::prelude::*;
 29use util::ResultExt;
 30use workspace::{Toast, Workspace, notifications::NotificationId};
 31
 32pub fn init(
 33    fs: Arc<dyn Fs>,
 34    prompt_builder: Arc<PromptBuilder>,
 35    telemetry: Arc<Telemetry>,
 36    cx: &mut App,
 37) {
 38    cx.set_global(TerminalInlineAssistant::new(fs, prompt_builder, telemetry));
 39}
 40
 41const DEFAULT_CONTEXT_LINES: usize = 50;
 42const PROMPT_HISTORY_MAX_LEN: usize = 20;
 43
 44pub struct TerminalInlineAssistant {
 45    next_assist_id: TerminalInlineAssistId,
 46    assists: HashMap<TerminalInlineAssistId, TerminalInlineAssist>,
 47    prompt_history: VecDeque<String>,
 48    telemetry: Option<Arc<Telemetry>>,
 49    fs: Arc<dyn Fs>,
 50    prompt_builder: Arc<PromptBuilder>,
 51}
 52
 53impl Global for TerminalInlineAssistant {}
 54
 55impl TerminalInlineAssistant {
 56    pub fn new(
 57        fs: Arc<dyn Fs>,
 58        prompt_builder: Arc<PromptBuilder>,
 59        telemetry: Arc<Telemetry>,
 60    ) -> Self {
 61        Self {
 62            next_assist_id: TerminalInlineAssistId::default(),
 63            assists: HashMap::default(),
 64            prompt_history: VecDeque::default(),
 65            telemetry: Some(telemetry),
 66            fs,
 67            prompt_builder,
 68        }
 69    }
 70
 71    pub fn assist(
 72        &mut self,
 73        terminal_view: &Entity<TerminalView>,
 74        workspace: WeakEntity<Workspace>,
 75        project: WeakEntity<Project>,
 76        prompt_store: Option<Entity<PromptStore>>,
 77        thread_store: Option<WeakEntity<HistoryStore>>,
 78        initial_prompt: Option<String>,
 79        window: &mut Window,
 80        cx: &mut App,
 81    ) {
 82        let terminal = terminal_view.read(cx).terminal().clone();
 83        let assist_id = self.next_assist_id.post_inc();
 84        let prompt_buffer = cx.new(|cx| {
 85            MultiBuffer::singleton(
 86                cx.new(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx)),
 87                cx,
 88            )
 89        });
 90        let context_store = cx.new(|_cx| ContextStore::new(project));
 91        let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
 92
 93        let prompt_editor = cx.new(|cx| {
 94            PromptEditor::new_terminal(
 95                assist_id,
 96                self.prompt_history.clone(),
 97                prompt_buffer.clone(),
 98                codegen,
 99                self.fs.clone(),
100                context_store.clone(),
101                workspace.clone(),
102                thread_store.clone(),
103                prompt_store.as_ref().map(|s| s.downgrade()),
104                window,
105                cx,
106            )
107        });
108        let prompt_editor_render = prompt_editor.clone();
109        let block = terminal_view::BlockProperties {
110            height: 4,
111            render: Box::new(move |_| prompt_editor_render.clone().into_any_element()),
112        };
113        terminal_view.update(cx, |terminal_view, cx| {
114            terminal_view.set_block_below_cursor(block, window, cx);
115        });
116
117        let terminal_assistant = TerminalInlineAssist::new(
118            assist_id,
119            terminal_view,
120            prompt_editor,
121            workspace.clone(),
122            context_store,
123            prompt_store,
124            window,
125            cx,
126        );
127
128        self.assists.insert(assist_id, terminal_assistant);
129
130        self.focus_assist(assist_id, window, cx);
131    }
132
133    fn focus_assist(
134        &mut self,
135        assist_id: TerminalInlineAssistId,
136        window: &mut Window,
137        cx: &mut App,
138    ) {
139        let assist = &self.assists[&assist_id];
140        if let Some(prompt_editor) = assist.prompt_editor.as_ref() {
141            prompt_editor.update(cx, |this, cx| {
142                this.editor.update(cx, |editor, cx| {
143                    window.focus(&editor.focus_handle(cx));
144                    editor.select_all(&SelectAll, window, cx);
145                });
146            });
147        }
148    }
149
150    fn handle_prompt_editor_event(
151        &mut self,
152        prompt_editor: Entity<PromptEditor<TerminalCodegen>>,
153        event: &PromptEditorEvent,
154        window: &mut Window,
155        cx: &mut App,
156    ) {
157        let assist_id = prompt_editor.read(cx).id();
158        match event {
159            PromptEditorEvent::StartRequested => {
160                self.start_assist(assist_id, cx);
161            }
162            PromptEditorEvent::StopRequested => {
163                self.stop_assist(assist_id, cx);
164            }
165            PromptEditorEvent::ConfirmRequested { execute } => {
166                self.finish_assist(assist_id, false, *execute, window, cx);
167            }
168            PromptEditorEvent::CancelRequested => {
169                self.finish_assist(assist_id, true, false, window, cx);
170            }
171            PromptEditorEvent::Resized { height_in_lines } => {
172                self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, window, cx);
173            }
174        }
175    }
176
177    fn start_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut App) {
178        let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
179            assist
180        } else {
181            return;
182        };
183
184        let Some(user_prompt) = assist
185            .prompt_editor
186            .as_ref()
187            .map(|editor| editor.read(cx).prompt(cx))
188        else {
189            return;
190        };
191
192        self.prompt_history.retain(|prompt| *prompt != user_prompt);
193        self.prompt_history.push_back(user_prompt);
194        if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN {
195            self.prompt_history.pop_front();
196        }
197
198        assist
199            .terminal
200            .update(cx, |terminal, cx| {
201                terminal
202                    .terminal()
203                    .update(cx, |terminal, _| terminal.input(CLEAR_INPUT.as_bytes()));
204            })
205            .log_err();
206
207        let codegen = assist.codegen.clone();
208        let Some(request_task) = self.request_for_inline_assist(assist_id, cx).log_err() else {
209            return;
210        };
211
212        codegen.update(cx, |codegen, cx| codegen.start(request_task, cx));
213    }
214
215    fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut App) {
216        let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
217            assist
218        } else {
219            return;
220        };
221
222        assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
223    }
224
225    fn request_for_inline_assist(
226        &self,
227        assist_id: TerminalInlineAssistId,
228        cx: &mut App,
229    ) -> Result<Task<LanguageModelRequest>> {
230        let assist = self.assists.get(&assist_id).context("invalid assist")?;
231
232        let shell = std::env::var("SHELL").ok();
233        let (latest_output, working_directory) = assist
234            .terminal
235            .update(cx, |terminal, cx| {
236                let terminal = terminal.entity().read(cx);
237                let latest_output = terminal.last_n_non_empty_lines(DEFAULT_CONTEXT_LINES);
238                let working_directory = terminal
239                    .working_directory()
240                    .map(|path| path.to_string_lossy().into_owned());
241                (latest_output, working_directory)
242            })
243            .ok()
244            .unwrap_or_default();
245
246        let prompt = self.prompt_builder.generate_terminal_assistant_prompt(
247            &assist
248                .prompt_editor
249                .clone()
250                .context("invalid assist")?
251                .read(cx)
252                .prompt(cx),
253            shell.as_deref(),
254            working_directory.as_deref(),
255            &latest_output,
256        )?;
257
258        let contexts = assist
259            .context_store
260            .read(cx)
261            .context()
262            .cloned()
263            .collect::<Vec<_>>();
264        let context_load_task = assist.workspace.update(cx, |workspace, cx| {
265            let project = workspace.project();
266            load_context(contexts, project, &assist.prompt_store, cx)
267        })?;
268
269        let ConfiguredModel { model, .. } = LanguageModelRegistry::read_global(cx)
270            .inline_assistant_model()
271            .context("No inline assistant model")?;
272
273        let temperature = AgentSettings::temperature_for_model(&model, cx);
274
275        Ok(cx.background_spawn(async move {
276            let mut request_message = LanguageModelRequestMessage {
277                role: Role::User,
278                content: vec![],
279                cache: false,
280            };
281
282            context_load_task
283                .await
284                .add_to_request_message(&mut request_message);
285
286            request_message.content.push(prompt.into());
287
288            LanguageModelRequest {
289                thread_id: None,
290                prompt_id: None,
291                mode: None,
292                intent: Some(CompletionIntent::TerminalInlineAssist),
293                messages: vec![request_message],
294                tools: Vec::new(),
295                tool_choice: None,
296                stop: Vec::new(),
297                temperature,
298                thinking_allowed: false,
299            }
300        }))
301    }
302
303    fn finish_assist(
304        &mut self,
305        assist_id: TerminalInlineAssistId,
306        undo: bool,
307        execute: bool,
308        window: &mut Window,
309        cx: &mut App,
310    ) {
311        self.dismiss_assist(assist_id, window, cx);
312
313        if let Some(assist) = self.assists.remove(&assist_id) {
314            assist
315                .terminal
316                .update(cx, |this, cx| {
317                    this.clear_block_below_cursor(cx);
318                    this.focus_handle(cx).focus(window);
319                })
320                .log_err();
321
322            if let Some(ConfiguredModel { model, .. }) =
323                LanguageModelRegistry::read_global(cx).inline_assistant_model()
324            {
325                let codegen = assist.codegen.read(cx);
326                let executor = cx.background_executor().clone();
327                report_assistant_event(
328                    AssistantEventData {
329                        conversation_id: None,
330                        kind: AssistantKind::InlineTerminal,
331                        message_id: codegen.message_id.clone(),
332                        phase: if undo {
333                            AssistantPhase::Rejected
334                        } else {
335                            AssistantPhase::Accepted
336                        },
337                        model: model.telemetry_id(),
338                        model_provider: model.provider_id().to_string(),
339                        response_latency: None,
340                        error_message: None,
341                        language_name: None,
342                    },
343                    codegen.telemetry.clone(),
344                    cx.http_client(),
345                    model.api_key(cx),
346                    &executor,
347                );
348            }
349
350            assist.codegen.update(cx, |codegen, cx| {
351                if undo {
352                    codegen.undo(cx);
353                } else if execute {
354                    codegen.complete(cx);
355                }
356            });
357        }
358    }
359
360    fn dismiss_assist(
361        &mut self,
362        assist_id: TerminalInlineAssistId,
363        window: &mut Window,
364        cx: &mut App,
365    ) -> bool {
366        let Some(assist) = self.assists.get_mut(&assist_id) else {
367            return false;
368        };
369        if assist.prompt_editor.is_none() {
370            return false;
371        }
372        assist.prompt_editor = None;
373        assist
374            .terminal
375            .update(cx, |this, cx| {
376                this.clear_block_below_cursor(cx);
377                this.focus_handle(cx).focus(window);
378            })
379            .is_ok()
380    }
381
382    fn insert_prompt_editor_into_terminal(
383        &mut self,
384        assist_id: TerminalInlineAssistId,
385        height: u8,
386        window: &mut Window,
387        cx: &mut App,
388    ) {
389        if let Some(assist) = self.assists.get_mut(&assist_id)
390            && let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned()
391        {
392            assist
393                .terminal
394                .update(cx, |terminal, cx| {
395                    terminal.clear_block_below_cursor(cx);
396                    let block = terminal_view::BlockProperties {
397                        height,
398                        render: Box::new(move |_| prompt_editor.clone().into_any_element()),
399                    };
400                    terminal.set_block_below_cursor(block, window, cx);
401                })
402                .log_err();
403        }
404    }
405}
406
407struct TerminalInlineAssist {
408    terminal: WeakEntity<TerminalView>,
409    prompt_editor: Option<Entity<PromptEditor<TerminalCodegen>>>,
410    codegen: Entity<TerminalCodegen>,
411    workspace: WeakEntity<Workspace>,
412    context_store: Entity<ContextStore>,
413    prompt_store: Option<Entity<PromptStore>>,
414    _subscriptions: Vec<Subscription>,
415}
416
417impl TerminalInlineAssist {
418    pub fn new(
419        assist_id: TerminalInlineAssistId,
420        terminal: &Entity<TerminalView>,
421        prompt_editor: Entity<PromptEditor<TerminalCodegen>>,
422        workspace: WeakEntity<Workspace>,
423        context_store: Entity<ContextStore>,
424        prompt_store: Option<Entity<PromptStore>>,
425        window: &mut Window,
426        cx: &mut App,
427    ) -> Self {
428        let codegen = prompt_editor.read(cx).codegen().clone();
429        Self {
430            terminal: terminal.downgrade(),
431            prompt_editor: Some(prompt_editor.clone()),
432            codegen: codegen.clone(),
433            workspace,
434            context_store,
435            prompt_store,
436            _subscriptions: vec![
437                window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| {
438                    TerminalInlineAssistant::update_global(cx, |this, cx| {
439                        this.handle_prompt_editor_event(prompt_editor, event, window, cx)
440                    })
441                }),
442                window.subscribe(&codegen, cx, move |codegen, event, window, cx| {
443                    TerminalInlineAssistant::update_global(cx, |this, cx| match event {
444                        CodegenEvent::Finished => {
445                            let assist = if let Some(assist) = this.assists.get(&assist_id) {
446                                assist
447                            } else {
448                                return;
449                            };
450
451                            if let CodegenStatus::Error(error) = &codegen.read(cx).status
452                                && assist.prompt_editor.is_none()
453                                && let Some(workspace) = assist.workspace.upgrade()
454                            {
455                                let error = format!("Terminal inline assistant error: {}", error);
456                                workspace.update(cx, |workspace, cx| {
457                                    struct InlineAssistantError;
458
459                                    let id = NotificationId::composite::<InlineAssistantError>(
460                                        assist_id.0,
461                                    );
462
463                                    workspace.show_toast(Toast::new(id, error), cx);
464                                })
465                            }
466
467                            if assist.prompt_editor.is_none() {
468                                this.finish_assist(assist_id, false, false, window, cx);
469                            }
470                        }
471                    })
472                }),
473            ],
474        }
475    }
476}