terminal_inline_assistant.rs

   1use crate::context::attach_context_to_message;
   2use crate::context_picker::ContextPicker;
   3use crate::context_store::ContextStore;
   4use crate::context_strip::ContextStrip;
   5use crate::inline_prompt_editor::{CodegenStatus, PromptEditorEvent, PromptMode};
   6use crate::prompts::PromptBuilder;
   7use crate::thread_store::ThreadStore;
   8use crate::ToggleContextPicker;
   9use crate::{assistant_settings::AssistantSettings, inline_prompt_editor::render_cancel_button};
  10use anyhow::{Context as _, Result};
  11use client::telemetry::Telemetry;
  12use collections::{HashMap, VecDeque};
  13use editor::{
  14    actions::{MoveDown, MoveUp, SelectAll},
  15    Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
  16};
  17use fs::Fs;
  18use futures::{channel::mpsc, SinkExt, StreamExt};
  19use gpui::{
  20    AppContext, Context, EventEmitter, FocusHandle, FocusableView, Global, Model, ModelContext,
  21    Subscription, Task, TextStyle, UpdateGlobal, View, WeakModel, WeakView,
  22};
  23use language::Buffer;
  24use language_model::{
  25    LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
  26};
  27use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
  28use language_models::report_assistant_event;
  29use settings::{update_settings_file, Settings};
  30use std::{cmp, sync::Arc, time::Instant};
  31use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
  32use terminal::Terminal;
  33use terminal_view::TerminalView;
  34use theme::ThemeSettings;
  35use ui::{prelude::*, text_for_action, IconButtonShape, PopoverMenuHandle, Tooltip};
  36use util::ResultExt;
  37use workspace::{notifications::NotificationId, Toast, Workspace};
  38
  39pub fn init(
  40    fs: Arc<dyn Fs>,
  41    prompt_builder: Arc<PromptBuilder>,
  42    telemetry: Arc<Telemetry>,
  43    cx: &mut AppContext,
  44) {
  45    cx.set_global(TerminalInlineAssistant::new(fs, prompt_builder, telemetry));
  46}
  47
  48const DEFAULT_CONTEXT_LINES: usize = 50;
  49const PROMPT_HISTORY_MAX_LEN: usize = 20;
  50
  51#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
  52struct TerminalInlineAssistId(usize);
  53
  54impl TerminalInlineAssistId {
  55    fn post_inc(&mut self) -> TerminalInlineAssistId {
  56        let id = *self;
  57        self.0 += 1;
  58        id
  59    }
  60}
  61
  62pub struct TerminalInlineAssistant {
  63    next_assist_id: TerminalInlineAssistId,
  64    assists: HashMap<TerminalInlineAssistId, TerminalInlineAssist>,
  65    prompt_history: VecDeque<String>,
  66    telemetry: Option<Arc<Telemetry>>,
  67    fs: Arc<dyn Fs>,
  68    prompt_builder: Arc<PromptBuilder>,
  69}
  70
  71impl Global for TerminalInlineAssistant {}
  72
  73impl TerminalInlineAssistant {
  74    pub fn new(
  75        fs: Arc<dyn Fs>,
  76        prompt_builder: Arc<PromptBuilder>,
  77        telemetry: Arc<Telemetry>,
  78    ) -> Self {
  79        Self {
  80            next_assist_id: TerminalInlineAssistId::default(),
  81            assists: HashMap::default(),
  82            prompt_history: VecDeque::default(),
  83            telemetry: Some(telemetry),
  84            fs,
  85            prompt_builder,
  86        }
  87    }
  88
  89    pub fn assist(
  90        &mut self,
  91        terminal_view: &View<TerminalView>,
  92        workspace: WeakView<Workspace>,
  93        thread_store: Option<WeakModel<ThreadStore>>,
  94        cx: &mut WindowContext,
  95    ) {
  96        let terminal = terminal_view.read(cx).terminal().clone();
  97        let assist_id = self.next_assist_id.post_inc();
  98        let prompt_buffer = cx.new_model(|cx| {
  99            MultiBuffer::singleton(cx.new_model(|cx| Buffer::local(String::new(), cx)), cx)
 100        });
 101        let context_store = cx.new_model(|_cx| ContextStore::new());
 102        let codegen = cx.new_model(|_| Codegen::new(terminal, self.telemetry.clone()));
 103
 104        let prompt_editor = cx.new_view(|cx| {
 105            PromptEditor::new(
 106                assist_id,
 107                self.prompt_history.clone(),
 108                prompt_buffer.clone(),
 109                codegen,
 110                self.fs.clone(),
 111                context_store.clone(),
 112                workspace.clone(),
 113                thread_store.clone(),
 114                cx,
 115            )
 116        });
 117        let prompt_editor_render = prompt_editor.clone();
 118        let block = terminal_view::BlockProperties {
 119            height: 2,
 120            render: Box::new(move |_| prompt_editor_render.clone().into_any_element()),
 121        };
 122        terminal_view.update(cx, |terminal_view, cx| {
 123            terminal_view.set_block_below_cursor(block, cx);
 124        });
 125
 126        let terminal_assistant = TerminalInlineAssist::new(
 127            assist_id,
 128            terminal_view,
 129            prompt_editor,
 130            workspace.clone(),
 131            context_store,
 132            cx,
 133        );
 134
 135        self.assists.insert(assist_id, terminal_assistant);
 136
 137        self.focus_assist(assist_id, cx);
 138    }
 139
 140    fn focus_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
 141        let assist = &self.assists[&assist_id];
 142        if let Some(prompt_editor) = assist.prompt_editor.as_ref() {
 143            prompt_editor.update(cx, |this, cx| {
 144                this.editor.update(cx, |editor, cx| {
 145                    editor.focus(cx);
 146                    editor.select_all(&SelectAll, cx);
 147                });
 148            });
 149        }
 150    }
 151
 152    fn handle_prompt_editor_event(
 153        &mut self,
 154        prompt_editor: View<PromptEditor>,
 155        event: &PromptEditorEvent,
 156        cx: &mut WindowContext,
 157    ) {
 158        let assist_id = prompt_editor.read(cx).id;
 159        match event {
 160            PromptEditorEvent::StartRequested => {
 161                self.start_assist(assist_id, cx);
 162            }
 163            PromptEditorEvent::StopRequested => {
 164                self.stop_assist(assist_id, cx);
 165            }
 166            PromptEditorEvent::ConfirmRequested { execute } => {
 167                self.finish_assist(assist_id, false, *execute, cx);
 168            }
 169            PromptEditorEvent::CancelRequested => {
 170                self.finish_assist(assist_id, true, false, cx);
 171            }
 172            PromptEditorEvent::DismissRequested => {
 173                self.dismiss_assist(assist_id, cx);
 174            }
 175            PromptEditorEvent::Resized { height_in_lines } => {
 176                self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, cx);
 177            }
 178        }
 179    }
 180
 181    fn start_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
 182        let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
 183            assist
 184        } else {
 185            return;
 186        };
 187
 188        let Some(user_prompt) = assist
 189            .prompt_editor
 190            .as_ref()
 191            .map(|editor| editor.read(cx).prompt(cx))
 192        else {
 193            return;
 194        };
 195
 196        self.prompt_history.retain(|prompt| *prompt != user_prompt);
 197        self.prompt_history.push_back(user_prompt.clone());
 198        if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN {
 199            self.prompt_history.pop_front();
 200        }
 201
 202        assist
 203            .terminal
 204            .update(cx, |terminal, cx| {
 205                terminal
 206                    .terminal()
 207                    .update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
 208            })
 209            .log_err();
 210
 211        let codegen = assist.codegen.clone();
 212        let Some(request) = self.request_for_inline_assist(assist_id, cx).log_err() else {
 213            return;
 214        };
 215
 216        codegen.update(cx, |codegen, cx| codegen.start(request, cx));
 217    }
 218
 219    fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
 220        let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
 221            assist
 222        } else {
 223            return;
 224        };
 225
 226        assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
 227    }
 228
 229    fn request_for_inline_assist(
 230        &self,
 231        assist_id: TerminalInlineAssistId,
 232        cx: &mut WindowContext,
 233    ) -> Result<LanguageModelRequest> {
 234        let assist = self.assists.get(&assist_id).context("invalid assist")?;
 235
 236        let shell = std::env::var("SHELL").ok();
 237        let (latest_output, working_directory) = assist
 238            .terminal
 239            .update(cx, |terminal, cx| {
 240                let terminal = terminal.model().read(cx);
 241                let latest_output = terminal.last_n_non_empty_lines(DEFAULT_CONTEXT_LINES);
 242                let working_directory = terminal
 243                    .working_directory()
 244                    .map(|path| path.to_string_lossy().to_string());
 245                (latest_output, working_directory)
 246            })
 247            .ok()
 248            .unwrap_or_default();
 249
 250        let prompt = self.prompt_builder.generate_terminal_assistant_prompt(
 251            &assist
 252                .prompt_editor
 253                .clone()
 254                .context("invalid assist")?
 255                .read(cx)
 256                .prompt(cx),
 257            shell.as_deref(),
 258            working_directory.as_deref(),
 259            &latest_output,
 260        )?;
 261
 262        let mut request_message = LanguageModelRequestMessage {
 263            role: Role::User,
 264            content: vec![],
 265            cache: false,
 266        };
 267
 268        let context = assist
 269            .context_store
 270            .update(cx, |this, _cx| this.context().clone());
 271        attach_context_to_message(&mut request_message, context);
 272
 273        request_message.content.push(prompt.into());
 274
 275        Ok(LanguageModelRequest {
 276            messages: vec![request_message],
 277            tools: Vec::new(),
 278            stop: Vec::new(),
 279            temperature: None,
 280        })
 281    }
 282
 283    fn finish_assist(
 284        &mut self,
 285        assist_id: TerminalInlineAssistId,
 286        undo: bool,
 287        execute: bool,
 288        cx: &mut WindowContext,
 289    ) {
 290        self.dismiss_assist(assist_id, cx);
 291
 292        if let Some(assist) = self.assists.remove(&assist_id) {
 293            assist
 294                .terminal
 295                .update(cx, |this, cx| {
 296                    this.clear_block_below_cursor(cx);
 297                    this.focus_handle(cx).focus(cx);
 298                })
 299                .log_err();
 300
 301            if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
 302                let codegen = assist.codegen.read(cx);
 303                let executor = cx.background_executor().clone();
 304                report_assistant_event(
 305                    AssistantEvent {
 306                        conversation_id: None,
 307                        kind: AssistantKind::InlineTerminal,
 308                        message_id: codegen.message_id.clone(),
 309                        phase: if undo {
 310                            AssistantPhase::Rejected
 311                        } else {
 312                            AssistantPhase::Accepted
 313                        },
 314                        model: model.telemetry_id(),
 315                        model_provider: model.provider_id().to_string(),
 316                        response_latency: None,
 317                        error_message: None,
 318                        language_name: None,
 319                    },
 320                    codegen.telemetry.clone(),
 321                    cx.http_client(),
 322                    model.api_key(cx),
 323                    &executor,
 324                );
 325            }
 326
 327            assist.codegen.update(cx, |codegen, cx| {
 328                if undo {
 329                    codegen.undo(cx);
 330                } else if execute {
 331                    codegen.complete(cx);
 332                }
 333            });
 334        }
 335    }
 336
 337    fn dismiss_assist(
 338        &mut self,
 339        assist_id: TerminalInlineAssistId,
 340        cx: &mut WindowContext,
 341    ) -> bool {
 342        let Some(assist) = self.assists.get_mut(&assist_id) else {
 343            return false;
 344        };
 345        if assist.prompt_editor.is_none() {
 346            return false;
 347        }
 348        assist.prompt_editor = None;
 349        assist
 350            .terminal
 351            .update(cx, |this, cx| {
 352                this.clear_block_below_cursor(cx);
 353                this.focus_handle(cx).focus(cx);
 354            })
 355            .is_ok()
 356    }
 357
 358    fn insert_prompt_editor_into_terminal(
 359        &mut self,
 360        assist_id: TerminalInlineAssistId,
 361        height: u8,
 362        cx: &mut WindowContext,
 363    ) {
 364        if let Some(assist) = self.assists.get_mut(&assist_id) {
 365            if let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned() {
 366                assist
 367                    .terminal
 368                    .update(cx, |terminal, cx| {
 369                        terminal.clear_block_below_cursor(cx);
 370                        let block = terminal_view::BlockProperties {
 371                            height,
 372                            render: Box::new(move |_| prompt_editor.clone().into_any_element()),
 373                        };
 374                        terminal.set_block_below_cursor(block, cx);
 375                    })
 376                    .log_err();
 377            }
 378        }
 379    }
 380}
 381
 382struct TerminalInlineAssist {
 383    terminal: WeakView<TerminalView>,
 384    prompt_editor: Option<View<PromptEditor>>,
 385    codegen: Model<Codegen>,
 386    workspace: WeakView<Workspace>,
 387    context_store: Model<ContextStore>,
 388    _subscriptions: Vec<Subscription>,
 389}
 390
 391impl TerminalInlineAssist {
 392    pub fn new(
 393        assist_id: TerminalInlineAssistId,
 394        terminal: &View<TerminalView>,
 395        prompt_editor: View<PromptEditor>,
 396        workspace: WeakView<Workspace>,
 397        context_store: Model<ContextStore>,
 398        cx: &mut WindowContext,
 399    ) -> Self {
 400        let codegen = prompt_editor.read(cx).codegen.clone();
 401        Self {
 402            terminal: terminal.downgrade(),
 403            prompt_editor: Some(prompt_editor.clone()),
 404            codegen: codegen.clone(),
 405            workspace: workspace.clone(),
 406            context_store,
 407            _subscriptions: vec![
 408                cx.subscribe(&prompt_editor, |prompt_editor, event, cx| {
 409                    TerminalInlineAssistant::update_global(cx, |this, cx| {
 410                        this.handle_prompt_editor_event(prompt_editor, event, cx)
 411                    })
 412                }),
 413                cx.subscribe(&codegen, move |codegen, event, cx| {
 414                    TerminalInlineAssistant::update_global(cx, |this, cx| match event {
 415                        CodegenEvent::Finished => {
 416                            let assist = if let Some(assist) = this.assists.get(&assist_id) {
 417                                assist
 418                            } else {
 419                                return;
 420                            };
 421
 422                            if let CodegenStatus::Error(error) = &codegen.read(cx).status {
 423                                if assist.prompt_editor.is_none() {
 424                                    if let Some(workspace) = assist.workspace.upgrade() {
 425                                        let error =
 426                                            format!("Terminal inline assistant error: {}", error);
 427                                        workspace.update(cx, |workspace, cx| {
 428                                            struct InlineAssistantError;
 429
 430                                            let id =
 431                                                NotificationId::composite::<InlineAssistantError>(
 432                                                    assist_id.0,
 433                                                );
 434
 435                                            workspace.show_toast(Toast::new(id, error), cx);
 436                                        })
 437                                    }
 438                                }
 439                            }
 440
 441                            if assist.prompt_editor.is_none() {
 442                                this.finish_assist(assist_id, false, false, cx);
 443                            }
 444                        }
 445                    })
 446                }),
 447            ],
 448        }
 449    }
 450}
 451
 452struct PromptEditor {
 453    id: TerminalInlineAssistId,
 454    height_in_lines: u8,
 455    editor: View<Editor>,
 456    context_strip: View<ContextStrip>,
 457    context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
 458    language_model_selector: View<LanguageModelSelector>,
 459    edited_since_done: bool,
 460    prompt_history: VecDeque<String>,
 461    prompt_history_ix: Option<usize>,
 462    pending_prompt: String,
 463    codegen: Model<Codegen>,
 464    _codegen_subscription: Subscription,
 465    editor_subscriptions: Vec<Subscription>,
 466}
 467
 468impl EventEmitter<PromptEditorEvent> for PromptEditor {}
 469
 470impl Render for PromptEditor {
 471    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 472        let mut buttons = Vec::new();
 473
 474        buttons.extend(render_cancel_button(
 475            (&self.codegen.read(cx).status).into(),
 476            self.edited_since_done,
 477            PromptMode::Generate {
 478                supports_execute: true,
 479            },
 480            cx,
 481        ));
 482
 483        v_flex()
 484            .border_y_1()
 485            .border_color(cx.theme().status().info_border)
 486            .py_2()
 487            .size_full()
 488            .child(
 489                h_flex()
 490                    .key_context("PromptEditor")
 491                    .bg(cx.theme().colors().editor_background)
 492                    .on_action(cx.listener(Self::toggle_context_picker))
 493                    .on_action(cx.listener(Self::confirm))
 494                    .on_action(cx.listener(Self::secondary_confirm))
 495                    .on_action(cx.listener(Self::cancel))
 496                    .on_action(cx.listener(Self::move_up))
 497                    .on_action(cx.listener(Self::move_down))
 498                    .child(
 499                        h_flex()
 500                            .w_12()
 501                            .justify_center()
 502                            .gap_2()
 503                            .child(LanguageModelSelectorPopoverMenu::new(
 504                                self.language_model_selector.clone(),
 505                                IconButton::new("context", IconName::SettingsAlt)
 506                                    .shape(IconButtonShape::Square)
 507                                    .icon_size(IconSize::Small)
 508                                    .icon_color(Color::Muted)
 509                                    .tooltip(move |cx| {
 510                                        Tooltip::with_meta(
 511                                            format!(
 512                                                "Using {}",
 513                                                LanguageModelRegistry::read_global(cx)
 514                                                    .active_model()
 515                                                    .map(|model| model.name().0)
 516                                                    .unwrap_or_else(|| "No model selected".into()),
 517                                            ),
 518                                            None,
 519                                            "Change Model",
 520                                            cx,
 521                                        )
 522                                    }),
 523                            ))
 524                            .children(
 525                                if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
 526                                    let error_message = SharedString::from(error.to_string());
 527                                    Some(
 528                                        div()
 529                                            .id("error")
 530                                            .tooltip(move |cx| {
 531                                                Tooltip::text(error_message.clone(), cx)
 532                                            })
 533                                            .child(
 534                                                Icon::new(IconName::XCircle)
 535                                                    .size(IconSize::Small)
 536                                                    .color(Color::Error),
 537                                            ),
 538                                    )
 539                                } else {
 540                                    None
 541                                },
 542                            ),
 543                    )
 544                    .child(div().flex_1().child(self.render_prompt_editor(cx)))
 545                    .child(h_flex().gap_1().pr_4().children(buttons)),
 546            )
 547            .child(h_flex().child(self.context_strip.clone()))
 548    }
 549}
 550
 551impl FocusableView for PromptEditor {
 552    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
 553        self.editor.focus_handle(cx)
 554    }
 555}
 556
 557impl PromptEditor {
 558    const MAX_LINES: u8 = 8;
 559
 560    #[allow(clippy::too_many_arguments)]
 561    fn new(
 562        id: TerminalInlineAssistId,
 563        prompt_history: VecDeque<String>,
 564        prompt_buffer: Model<MultiBuffer>,
 565        codegen: Model<Codegen>,
 566        fs: Arc<dyn Fs>,
 567        context_store: Model<ContextStore>,
 568        workspace: WeakView<Workspace>,
 569        thread_store: Option<WeakModel<ThreadStore>>,
 570        cx: &mut ViewContext<Self>,
 571    ) -> Self {
 572        let prompt_editor = cx.new_view(|cx| {
 573            let mut editor = Editor::new(
 574                EditorMode::AutoHeight {
 575                    max_lines: Self::MAX_LINES as usize,
 576                },
 577                prompt_buffer,
 578                None,
 579                false,
 580                cx,
 581            );
 582            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
 583            editor.set_placeholder_text(Self::placeholder_text(cx), cx);
 584            editor
 585        });
 586        let context_picker_menu_handle = PopoverMenuHandle::default();
 587
 588        let mut this = Self {
 589            id,
 590            height_in_lines: 1,
 591            editor: prompt_editor.clone(),
 592            context_strip: cx.new_view(|cx| {
 593                ContextStrip::new(
 594                    context_store,
 595                    workspace.clone(),
 596                    thread_store.clone(),
 597                    prompt_editor.focus_handle(cx),
 598                    context_picker_menu_handle.clone(),
 599                    cx,
 600                )
 601            }),
 602            context_picker_menu_handle,
 603            language_model_selector: cx.new_view(|cx| {
 604                let fs = fs.clone();
 605                LanguageModelSelector::new(
 606                    move |model, cx| {
 607                        update_settings_file::<AssistantSettings>(
 608                            fs.clone(),
 609                            cx,
 610                            move |settings, _| settings.set_model(model.clone()),
 611                        );
 612                    },
 613                    cx,
 614                )
 615            }),
 616            edited_since_done: false,
 617            prompt_history,
 618            prompt_history_ix: None,
 619            pending_prompt: String::new(),
 620            _codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
 621            editor_subscriptions: Vec::new(),
 622            codegen,
 623        };
 624        this.count_lines(cx);
 625        this.subscribe_to_editor(cx);
 626        this
 627    }
 628
 629    fn placeholder_text(cx: &WindowContext) -> String {
 630        let context_keybinding = text_for_action(&crate::ToggleFocus, cx)
 631            .map(|keybinding| format!("{keybinding} for context"))
 632            .unwrap_or_default();
 633
 634        format!("Generate…{context_keybinding} ↓↑ for history")
 635    }
 636
 637    fn subscribe_to_editor(&mut self, cx: &mut ViewContext<Self>) {
 638        self.editor_subscriptions.clear();
 639        self.editor_subscriptions
 640            .push(cx.observe(&self.editor, Self::handle_prompt_editor_changed));
 641        self.editor_subscriptions
 642            .push(cx.subscribe(&self.editor, Self::handle_prompt_editor_events));
 643    }
 644
 645    fn prompt(&self, cx: &AppContext) -> String {
 646        self.editor.read(cx).text(cx)
 647    }
 648
 649    fn count_lines(&mut self, cx: &mut ViewContext<Self>) {
 650        let height_in_lines = cmp::max(
 651            2, // Make the editor at least two lines tall, to account for padding and buttons.
 652            cmp::min(
 653                self.editor
 654                    .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
 655                Self::MAX_LINES as u32,
 656            ),
 657        ) as u8;
 658
 659        if height_in_lines != self.height_in_lines {
 660            self.height_in_lines = height_in_lines;
 661            cx.emit(PromptEditorEvent::Resized { height_in_lines });
 662        }
 663    }
 664
 665    fn handle_prompt_editor_changed(&mut self, _: View<Editor>, cx: &mut ViewContext<Self>) {
 666        self.count_lines(cx);
 667    }
 668
 669    fn handle_prompt_editor_events(
 670        &mut self,
 671        _: View<Editor>,
 672        event: &EditorEvent,
 673        cx: &mut ViewContext<Self>,
 674    ) {
 675        match event {
 676            EditorEvent::Edited { .. } => {
 677                let prompt = self.editor.read(cx).text(cx);
 678                if self
 679                    .prompt_history_ix
 680                    .map_or(true, |ix| self.prompt_history[ix] != prompt)
 681                {
 682                    self.prompt_history_ix.take();
 683                    self.pending_prompt = prompt;
 684                }
 685
 686                self.edited_since_done = true;
 687                cx.notify();
 688            }
 689            _ => {}
 690        }
 691    }
 692
 693    fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
 694        match &self.codegen.read(cx).status {
 695            CodegenStatus::Idle => {
 696                self.editor
 697                    .update(cx, |editor, _| editor.set_read_only(false));
 698            }
 699            CodegenStatus::Pending => {
 700                self.editor
 701                    .update(cx, |editor, _| editor.set_read_only(true));
 702            }
 703            CodegenStatus::Done | CodegenStatus::Error(_) => {
 704                self.edited_since_done = false;
 705                self.editor
 706                    .update(cx, |editor, _| editor.set_read_only(false));
 707            }
 708        }
 709    }
 710
 711    fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
 712        self.context_picker_menu_handle.toggle(cx);
 713    }
 714
 715    fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
 716        match &self.codegen.read(cx).status {
 717            CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
 718                cx.emit(PromptEditorEvent::CancelRequested);
 719            }
 720            CodegenStatus::Pending => {
 721                cx.emit(PromptEditorEvent::StopRequested);
 722            }
 723        }
 724    }
 725
 726    fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
 727        match &self.codegen.read(cx).status {
 728            CodegenStatus::Idle => {
 729                if !self.editor.read(cx).text(cx).trim().is_empty() {
 730                    cx.emit(PromptEditorEvent::StartRequested);
 731                }
 732            }
 733            CodegenStatus::Pending => {
 734                cx.emit(PromptEditorEvent::DismissRequested);
 735            }
 736            CodegenStatus::Done => {
 737                if self.edited_since_done {
 738                    cx.emit(PromptEditorEvent::StartRequested);
 739                } else {
 740                    cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
 741                }
 742            }
 743            CodegenStatus::Error(_) => {
 744                cx.emit(PromptEditorEvent::StartRequested);
 745            }
 746        }
 747    }
 748
 749    fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
 750        if matches!(self.codegen.read(cx).status, CodegenStatus::Done) {
 751            cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
 752        }
 753    }
 754
 755    fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
 756        if let Some(ix) = self.prompt_history_ix {
 757            if ix > 0 {
 758                self.prompt_history_ix = Some(ix - 1);
 759                let prompt = self.prompt_history[ix - 1].as_str();
 760                self.editor.update(cx, |editor, cx| {
 761                    editor.set_text(prompt, cx);
 762                    editor.move_to_beginning(&Default::default(), cx);
 763                });
 764            }
 765        } else if !self.prompt_history.is_empty() {
 766            self.prompt_history_ix = Some(self.prompt_history.len() - 1);
 767            let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
 768            self.editor.update(cx, |editor, cx| {
 769                editor.set_text(prompt, cx);
 770                editor.move_to_beginning(&Default::default(), cx);
 771            });
 772        }
 773    }
 774
 775    fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
 776        if let Some(ix) = self.prompt_history_ix {
 777            if ix < self.prompt_history.len() - 1 {
 778                self.prompt_history_ix = Some(ix + 1);
 779                let prompt = self.prompt_history[ix + 1].as_str();
 780                self.editor.update(cx, |editor, cx| {
 781                    editor.set_text(prompt, cx);
 782                    editor.move_to_end(&Default::default(), cx)
 783                });
 784            } else {
 785                self.prompt_history_ix = None;
 786                let prompt = self.pending_prompt.as_str();
 787                self.editor.update(cx, |editor, cx| {
 788                    editor.set_text(prompt, cx);
 789                    editor.move_to_end(&Default::default(), cx)
 790                });
 791            }
 792        }
 793    }
 794
 795    fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 796        let settings = ThemeSettings::get_global(cx);
 797        let text_style = TextStyle {
 798            color: if self.editor.read(cx).read_only(cx) {
 799                cx.theme().colors().text_disabled
 800            } else {
 801                cx.theme().colors().text
 802            },
 803            font_family: settings.buffer_font.family.clone(),
 804            font_fallbacks: settings.buffer_font.fallbacks.clone(),
 805            font_size: settings.buffer_font_size.into(),
 806            font_weight: settings.buffer_font.weight,
 807            line_height: relative(settings.buffer_line_height.value()),
 808            ..Default::default()
 809        };
 810        EditorElement::new(
 811            &self.editor,
 812            EditorStyle {
 813                background: cx.theme().colors().editor_background,
 814                local_player: cx.theme().players().local(),
 815                text: text_style,
 816                ..Default::default()
 817            },
 818        )
 819    }
 820}
 821
 822#[derive(Debug)]
 823pub enum CodegenEvent {
 824    Finished,
 825}
 826
 827impl EventEmitter<CodegenEvent> for Codegen {}
 828
 829const CLEAR_INPUT: &str = "\x15";
 830const CARRIAGE_RETURN: &str = "\x0d";
 831
 832struct TerminalTransaction {
 833    terminal: Model<Terminal>,
 834}
 835
 836impl TerminalTransaction {
 837    pub fn start(terminal: Model<Terminal>) -> Self {
 838        Self { terminal }
 839    }
 840
 841    pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
 842        // Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
 843        let input = Self::sanitize_input(hunk);
 844        self.terminal
 845            .update(cx, |terminal, _| terminal.input(input));
 846    }
 847
 848    pub fn undo(&self, cx: &mut AppContext) {
 849        self.terminal
 850            .update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
 851    }
 852
 853    pub fn complete(&self, cx: &mut AppContext) {
 854        self.terminal.update(cx, |terminal, _| {
 855            terminal.input(CARRIAGE_RETURN.to_string())
 856        });
 857    }
 858
 859    fn sanitize_input(input: String) -> String {
 860        input.replace(['\r', '\n'], "")
 861    }
 862}
 863
 864pub struct Codegen {
 865    status: CodegenStatus,
 866    telemetry: Option<Arc<Telemetry>>,
 867    terminal: Model<Terminal>,
 868    generation: Task<()>,
 869    message_id: Option<String>,
 870    transaction: Option<TerminalTransaction>,
 871}
 872
 873impl Codegen {
 874    pub fn new(terminal: Model<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
 875        Self {
 876            terminal,
 877            telemetry,
 878            status: CodegenStatus::Idle,
 879            generation: Task::ready(()),
 880            message_id: None,
 881            transaction: None,
 882        }
 883    }
 884
 885    pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
 886        let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
 887            return;
 888        };
 889
 890        let model_api_key = model.api_key(cx);
 891        let http_client = cx.http_client();
 892        let telemetry = self.telemetry.clone();
 893        self.status = CodegenStatus::Pending;
 894        self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
 895        self.generation = cx.spawn(|this, mut cx| async move {
 896            let model_telemetry_id = model.telemetry_id();
 897            let model_provider_id = model.provider_id();
 898            let response = model.stream_completion_text(prompt, &cx).await;
 899            let generate = async {
 900                let message_id = response
 901                    .as_ref()
 902                    .ok()
 903                    .and_then(|response| response.message_id.clone());
 904
 905                let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
 906
 907                let task = cx.background_executor().spawn({
 908                    let message_id = message_id.clone();
 909                    let executor = cx.background_executor().clone();
 910                    async move {
 911                        let mut response_latency = None;
 912                        let request_start = Instant::now();
 913                        let task = async {
 914                            let mut chunks = response?.stream;
 915                            while let Some(chunk) = chunks.next().await {
 916                                if response_latency.is_none() {
 917                                    response_latency = Some(request_start.elapsed());
 918                                }
 919                                let chunk = chunk?;
 920                                hunks_tx.send(chunk).await?;
 921                            }
 922
 923                            anyhow::Ok(())
 924                        };
 925
 926                        let result = task.await;
 927
 928                        let error_message = result.as_ref().err().map(|error| error.to_string());
 929                        report_assistant_event(
 930                            AssistantEvent {
 931                                conversation_id: None,
 932                                kind: AssistantKind::InlineTerminal,
 933                                message_id,
 934                                phase: AssistantPhase::Response,
 935                                model: model_telemetry_id,
 936                                model_provider: model_provider_id.to_string(),
 937                                response_latency,
 938                                error_message,
 939                                language_name: None,
 940                            },
 941                            telemetry,
 942                            http_client,
 943                            model_api_key,
 944                            &executor,
 945                        );
 946
 947                        result?;
 948                        anyhow::Ok(())
 949                    }
 950                });
 951
 952                this.update(&mut cx, |this, _| {
 953                    this.message_id = message_id;
 954                })?;
 955
 956                while let Some(hunk) = hunks_rx.next().await {
 957                    this.update(&mut cx, |this, cx| {
 958                        if let Some(transaction) = &mut this.transaction {
 959                            transaction.push(hunk, cx);
 960                            cx.notify();
 961                        }
 962                    })?;
 963                }
 964
 965                task.await?;
 966                anyhow::Ok(())
 967            };
 968
 969            let result = generate.await;
 970
 971            this.update(&mut cx, |this, cx| {
 972                if let Err(error) = result {
 973                    this.status = CodegenStatus::Error(error);
 974                } else {
 975                    this.status = CodegenStatus::Done;
 976                }
 977                cx.emit(CodegenEvent::Finished);
 978                cx.notify();
 979            })
 980            .ok();
 981        });
 982        cx.notify();
 983    }
 984
 985    pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
 986        self.status = CodegenStatus::Done;
 987        self.generation = Task::ready(());
 988        cx.emit(CodegenEvent::Finished);
 989        cx.notify();
 990    }
 991
 992    pub fn complete(&mut self, cx: &mut ModelContext<Self>) {
 993        if let Some(transaction) = self.transaction.take() {
 994            transaction.complete(cx);
 995        }
 996    }
 997
 998    pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
 999        if let Some(transaction) = self.transaction.take() {
1000            transaction.undo(cx);
1001        }
1002    }
1003}