terminal_inline_assistant.rs

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