terminal_inline_assistant.rs

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