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: Option<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: Option<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: Option<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
 400                                        .workspace
 401                                        .as_ref()
 402                                        .and_then(|workspace| workspace.upgrade())
 403                                    {
 404                                        let error =
 405                                            format!("Terminal inline assistant error: {}", error);
 406                                        workspace.update(cx, |workspace, cx| {
 407                                            struct InlineAssistantError;
 408
 409                                            let id =
 410                                                NotificationId::composite::<InlineAssistantError>(
 411                                                    assist_id.0,
 412                                                );
 413
 414                                            workspace.show_toast(Toast::new(id, error), cx);
 415                                        })
 416                                    }
 417                                }
 418                            }
 419
 420                            if assist.prompt_editor.is_none() {
 421                                this.finish_assist(assist_id, false, false, cx);
 422                            }
 423                        }
 424                    })
 425                }),
 426            ],
 427        }
 428    }
 429}
 430
 431enum PromptEditorEvent {
 432    StartRequested,
 433    StopRequested,
 434    ConfirmRequested { execute: bool },
 435    CancelRequested,
 436    DismissRequested,
 437    Resized { height_in_lines: u8 },
 438}
 439
 440struct PromptEditor {
 441    id: TerminalInlineAssistId,
 442    height_in_lines: u8,
 443    editor: View<Editor>,
 444    language_model_selector: View<LanguageModelSelector>,
 445    edited_since_done: bool,
 446    prompt_history: VecDeque<String>,
 447    prompt_history_ix: Option<usize>,
 448    pending_prompt: String,
 449    codegen: Model<Codegen>,
 450    _codegen_subscription: Subscription,
 451    editor_subscriptions: Vec<Subscription>,
 452}
 453
 454impl EventEmitter<PromptEditorEvent> for PromptEditor {}
 455
 456impl Render for PromptEditor {
 457    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 458        let status = &self.codegen.read(cx).status;
 459        let mut buttons = vec![Button::new("add-context", "Add Context")
 460            .style(ButtonStyle::Filled)
 461            .icon(IconName::Plus)
 462            .icon_position(IconPosition::Start)
 463            .into_any_element()];
 464
 465        buttons.extend(match status {
 466            CodegenStatus::Idle => vec![
 467                IconButton::new("cancel", IconName::Close)
 468                    .icon_color(Color::Muted)
 469                    .shape(IconButtonShape::Square)
 470                    .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
 471                    .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
 472                    .into_any_element(),
 473                IconButton::new("start", IconName::SparkleAlt)
 474                    .icon_color(Color::Muted)
 475                    .shape(IconButtonShape::Square)
 476                    .tooltip(|cx| Tooltip::for_action("Generate", &menu::Confirm, cx))
 477                    .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)))
 478                    .into_any_element(),
 479            ],
 480            CodegenStatus::Pending => vec![
 481                IconButton::new("cancel", IconName::Close)
 482                    .icon_color(Color::Muted)
 483                    .shape(IconButtonShape::Square)
 484                    .tooltip(|cx| Tooltip::text("Cancel Assist", cx))
 485                    .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
 486                    .into_any_element(),
 487                IconButton::new("stop", IconName::Stop)
 488                    .icon_color(Color::Error)
 489                    .shape(IconButtonShape::Square)
 490                    .tooltip(|cx| {
 491                        Tooltip::with_meta(
 492                            "Interrupt Generation",
 493                            Some(&menu::Cancel),
 494                            "Changes won't be discarded",
 495                            cx,
 496                        )
 497                    })
 498                    .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
 499                    .into_any_element(),
 500            ],
 501            CodegenStatus::Error(_) | CodegenStatus::Done => {
 502                let cancel = IconButton::new("cancel", IconName::Close)
 503                    .icon_color(Color::Muted)
 504                    .shape(IconButtonShape::Square)
 505                    .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
 506                    .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
 507                    .into_any_element();
 508
 509                let has_error = matches!(status, CodegenStatus::Error(_));
 510                if has_error || self.edited_since_done {
 511                    vec![
 512                        cancel,
 513                        IconButton::new("restart", IconName::RotateCw)
 514                            .icon_color(Color::Info)
 515                            .shape(IconButtonShape::Square)
 516                            .tooltip(|cx| {
 517                                Tooltip::with_meta(
 518                                    "Restart Generation",
 519                                    Some(&menu::Confirm),
 520                                    "Changes will be discarded",
 521                                    cx,
 522                                )
 523                            })
 524                            .on_click(cx.listener(|_, _, cx| {
 525                                cx.emit(PromptEditorEvent::StartRequested);
 526                            }))
 527                            .into_any_element(),
 528                    ]
 529                } else {
 530                    vec![
 531                        cancel,
 532                        IconButton::new("accept", IconName::Check)
 533                            .icon_color(Color::Info)
 534                            .shape(IconButtonShape::Square)
 535                            .tooltip(|cx| {
 536                                Tooltip::for_action("Accept Generated Command", &menu::Confirm, cx)
 537                            })
 538                            .on_click(cx.listener(|_, _, cx| {
 539                                cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
 540                            }))
 541                            .into_any_element(),
 542                        IconButton::new("confirm", IconName::Play)
 543                            .icon_color(Color::Info)
 544                            .shape(IconButtonShape::Square)
 545                            .tooltip(|cx| {
 546                                Tooltip::for_action(
 547                                    "Execute Generated Command",
 548                                    &menu::SecondaryConfirm,
 549                                    cx,
 550                                )
 551                            })
 552                            .on_click(cx.listener(|_, _, cx| {
 553                                cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
 554                            }))
 555                            .into_any_element(),
 556                    ]
 557                }
 558            }
 559        });
 560
 561        h_flex()
 562            .bg(cx.theme().colors().editor_background)
 563            .border_y_1()
 564            .border_color(cx.theme().status().info_border)
 565            .py_2()
 566            .h_full()
 567            .w_full()
 568            .on_action(cx.listener(Self::confirm))
 569            .on_action(cx.listener(Self::secondary_confirm))
 570            .on_action(cx.listener(Self::cancel))
 571            .on_action(cx.listener(Self::move_up))
 572            .on_action(cx.listener(Self::move_down))
 573            .child(
 574                h_flex()
 575                    .w_12()
 576                    .justify_center()
 577                    .gap_2()
 578                    .child(LanguageModelSelectorPopoverMenu::new(
 579                        self.language_model_selector.clone(),
 580                        IconButton::new("context", IconName::SettingsAlt)
 581                            .shape(IconButtonShape::Square)
 582                            .icon_size(IconSize::Small)
 583                            .icon_color(Color::Muted)
 584                            .tooltip(move |cx| {
 585                                Tooltip::with_meta(
 586                                    format!(
 587                                        "Using {}",
 588                                        LanguageModelRegistry::read_global(cx)
 589                                            .active_model()
 590                                            .map(|model| model.name().0)
 591                                            .unwrap_or_else(|| "No model selected".into()),
 592                                    ),
 593                                    None,
 594                                    "Change Model",
 595                                    cx,
 596                                )
 597                            }),
 598                    ))
 599                    .children(
 600                        if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
 601                            let error_message = SharedString::from(error.to_string());
 602                            Some(
 603                                div()
 604                                    .id("error")
 605                                    .tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
 606                                    .child(
 607                                        Icon::new(IconName::XCircle)
 608                                            .size(IconSize::Small)
 609                                            .color(Color::Error),
 610                                    ),
 611                            )
 612                        } else {
 613                            None
 614                        },
 615                    ),
 616            )
 617            .child(div().flex_1().child(self.render_prompt_editor(cx)))
 618            .child(h_flex().gap_1().pr_4().children(buttons))
 619    }
 620}
 621
 622impl FocusableView for PromptEditor {
 623    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
 624        self.editor.focus_handle(cx)
 625    }
 626}
 627
 628impl PromptEditor {
 629    const MAX_LINES: u8 = 8;
 630
 631    #[allow(clippy::too_many_arguments)]
 632    fn new(
 633        id: TerminalInlineAssistId,
 634        prompt_history: VecDeque<String>,
 635        prompt_buffer: Model<MultiBuffer>,
 636        codegen: Model<Codegen>,
 637        fs: Arc<dyn Fs>,
 638        cx: &mut ViewContext<Self>,
 639    ) -> Self {
 640        let prompt_editor = cx.new_view(|cx| {
 641            let mut editor = Editor::new(
 642                EditorMode::AutoHeight {
 643                    max_lines: Self::MAX_LINES as usize,
 644                },
 645                prompt_buffer,
 646                None,
 647                false,
 648                cx,
 649            );
 650            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
 651            editor.set_placeholder_text(Self::placeholder_text(cx), cx);
 652            editor
 653        });
 654
 655        let mut this = Self {
 656            id,
 657            height_in_lines: 1,
 658            editor: prompt_editor,
 659            language_model_selector: cx.new_view(|cx| {
 660                let fs = fs.clone();
 661                LanguageModelSelector::new(
 662                    move |model, cx| {
 663                        update_settings_file::<AssistantSettings>(
 664                            fs.clone(),
 665                            cx,
 666                            move |settings, _| settings.set_model(model.clone()),
 667                        );
 668                    },
 669                    cx,
 670                )
 671            }),
 672            edited_since_done: false,
 673            prompt_history,
 674            prompt_history_ix: None,
 675            pending_prompt: String::new(),
 676            _codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
 677            editor_subscriptions: Vec::new(),
 678            codegen,
 679        };
 680        this.count_lines(cx);
 681        this.subscribe_to_editor(cx);
 682        this
 683    }
 684
 685    fn placeholder_text(cx: &WindowContext) -> String {
 686        let context_keybinding = text_for_action(&crate::ToggleFocus, cx)
 687            .map(|keybinding| format!("{keybinding} for context"))
 688            .unwrap_or_default();
 689
 690        format!("Generate…{context_keybinding} ↓↑ for history")
 691    }
 692
 693    fn subscribe_to_editor(&mut self, cx: &mut ViewContext<Self>) {
 694        self.editor_subscriptions.clear();
 695        self.editor_subscriptions
 696            .push(cx.observe(&self.editor, Self::handle_prompt_editor_changed));
 697        self.editor_subscriptions
 698            .push(cx.subscribe(&self.editor, Self::handle_prompt_editor_events));
 699    }
 700
 701    fn prompt(&self, cx: &AppContext) -> String {
 702        self.editor.read(cx).text(cx)
 703    }
 704
 705    fn count_lines(&mut self, cx: &mut ViewContext<Self>) {
 706        let height_in_lines = cmp::max(
 707            2, // Make the editor at least two lines tall, to account for padding and buttons.
 708            cmp::min(
 709                self.editor
 710                    .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
 711                Self::MAX_LINES as u32,
 712            ),
 713        ) as u8;
 714
 715        if height_in_lines != self.height_in_lines {
 716            self.height_in_lines = height_in_lines;
 717            cx.emit(PromptEditorEvent::Resized { height_in_lines });
 718        }
 719    }
 720
 721    fn handle_prompt_editor_changed(&mut self, _: View<Editor>, cx: &mut ViewContext<Self>) {
 722        self.count_lines(cx);
 723    }
 724
 725    fn handle_prompt_editor_events(
 726        &mut self,
 727        _: View<Editor>,
 728        event: &EditorEvent,
 729        cx: &mut ViewContext<Self>,
 730    ) {
 731        match event {
 732            EditorEvent::Edited { .. } => {
 733                let prompt = self.editor.read(cx).text(cx);
 734                if self
 735                    .prompt_history_ix
 736                    .map_or(true, |ix| self.prompt_history[ix] != prompt)
 737                {
 738                    self.prompt_history_ix.take();
 739                    self.pending_prompt = prompt;
 740                }
 741
 742                self.edited_since_done = true;
 743                cx.notify();
 744            }
 745            _ => {}
 746        }
 747    }
 748
 749    fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
 750        match &self.codegen.read(cx).status {
 751            CodegenStatus::Idle => {
 752                self.editor
 753                    .update(cx, |editor, _| editor.set_read_only(false));
 754            }
 755            CodegenStatus::Pending => {
 756                self.editor
 757                    .update(cx, |editor, _| editor.set_read_only(true));
 758            }
 759            CodegenStatus::Done | CodegenStatus::Error(_) => {
 760                self.edited_since_done = false;
 761                self.editor
 762                    .update(cx, |editor, _| editor.set_read_only(false));
 763            }
 764        }
 765    }
 766
 767    fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
 768        match &self.codegen.read(cx).status {
 769            CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
 770                cx.emit(PromptEditorEvent::CancelRequested);
 771            }
 772            CodegenStatus::Pending => {
 773                cx.emit(PromptEditorEvent::StopRequested);
 774            }
 775        }
 776    }
 777
 778    fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
 779        match &self.codegen.read(cx).status {
 780            CodegenStatus::Idle => {
 781                if !self.editor.read(cx).text(cx).trim().is_empty() {
 782                    cx.emit(PromptEditorEvent::StartRequested);
 783                }
 784            }
 785            CodegenStatus::Pending => {
 786                cx.emit(PromptEditorEvent::DismissRequested);
 787            }
 788            CodegenStatus::Done => {
 789                if self.edited_since_done {
 790                    cx.emit(PromptEditorEvent::StartRequested);
 791                } else {
 792                    cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
 793                }
 794            }
 795            CodegenStatus::Error(_) => {
 796                cx.emit(PromptEditorEvent::StartRequested);
 797            }
 798        }
 799    }
 800
 801    fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
 802        if matches!(self.codegen.read(cx).status, CodegenStatus::Done) {
 803            cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
 804        }
 805    }
 806
 807    fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
 808        if let Some(ix) = self.prompt_history_ix {
 809            if ix > 0 {
 810                self.prompt_history_ix = Some(ix - 1);
 811                let prompt = self.prompt_history[ix - 1].as_str();
 812                self.editor.update(cx, |editor, cx| {
 813                    editor.set_text(prompt, cx);
 814                    editor.move_to_beginning(&Default::default(), cx);
 815                });
 816            }
 817        } else if !self.prompt_history.is_empty() {
 818            self.prompt_history_ix = Some(self.prompt_history.len() - 1);
 819            let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
 820            self.editor.update(cx, |editor, cx| {
 821                editor.set_text(prompt, cx);
 822                editor.move_to_beginning(&Default::default(), cx);
 823            });
 824        }
 825    }
 826
 827    fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
 828        if let Some(ix) = self.prompt_history_ix {
 829            if ix < self.prompt_history.len() - 1 {
 830                self.prompt_history_ix = Some(ix + 1);
 831                let prompt = self.prompt_history[ix + 1].as_str();
 832                self.editor.update(cx, |editor, cx| {
 833                    editor.set_text(prompt, cx);
 834                    editor.move_to_end(&Default::default(), cx)
 835                });
 836            } else {
 837                self.prompt_history_ix = None;
 838                let prompt = self.pending_prompt.as_str();
 839                self.editor.update(cx, |editor, cx| {
 840                    editor.set_text(prompt, cx);
 841                    editor.move_to_end(&Default::default(), cx)
 842                });
 843            }
 844        }
 845    }
 846
 847    fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 848        let settings = ThemeSettings::get_global(cx);
 849        let text_style = TextStyle {
 850            color: if self.editor.read(cx).read_only(cx) {
 851                cx.theme().colors().text_disabled
 852            } else {
 853                cx.theme().colors().text
 854            },
 855            font_family: settings.buffer_font.family.clone(),
 856            font_fallbacks: settings.buffer_font.fallbacks.clone(),
 857            font_size: settings.buffer_font_size.into(),
 858            font_weight: settings.buffer_font.weight,
 859            line_height: relative(settings.buffer_line_height.value()),
 860            ..Default::default()
 861        };
 862        EditorElement::new(
 863            &self.editor,
 864            EditorStyle {
 865                background: cx.theme().colors().editor_background,
 866                local_player: cx.theme().players().local(),
 867                text: text_style,
 868                ..Default::default()
 869            },
 870        )
 871    }
 872}
 873
 874#[derive(Debug)]
 875pub enum CodegenEvent {
 876    Finished,
 877}
 878
 879impl EventEmitter<CodegenEvent> for Codegen {}
 880
 881const CLEAR_INPUT: &str = "\x15";
 882const CARRIAGE_RETURN: &str = "\x0d";
 883
 884struct TerminalTransaction {
 885    terminal: Model<Terminal>,
 886}
 887
 888impl TerminalTransaction {
 889    pub fn start(terminal: Model<Terminal>) -> Self {
 890        Self { terminal }
 891    }
 892
 893    pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
 894        // Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
 895        let input = Self::sanitize_input(hunk);
 896        self.terminal
 897            .update(cx, |terminal, _| terminal.input(input));
 898    }
 899
 900    pub fn undo(&self, cx: &mut AppContext) {
 901        self.terminal
 902            .update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
 903    }
 904
 905    pub fn complete(&self, cx: &mut AppContext) {
 906        self.terminal.update(cx, |terminal, _| {
 907            terminal.input(CARRIAGE_RETURN.to_string())
 908        });
 909    }
 910
 911    fn sanitize_input(input: String) -> String {
 912        input.replace(['\r', '\n'], "")
 913    }
 914}
 915
 916pub struct Codegen {
 917    status: CodegenStatus,
 918    telemetry: Option<Arc<Telemetry>>,
 919    terminal: Model<Terminal>,
 920    generation: Task<()>,
 921    message_id: Option<String>,
 922    transaction: Option<TerminalTransaction>,
 923}
 924
 925impl Codegen {
 926    pub fn new(terminal: Model<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
 927        Self {
 928            terminal,
 929            telemetry,
 930            status: CodegenStatus::Idle,
 931            generation: Task::ready(()),
 932            message_id: None,
 933            transaction: None,
 934        }
 935    }
 936
 937    pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
 938        let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
 939            return;
 940        };
 941
 942        let model_api_key = model.api_key(cx);
 943        let http_client = cx.http_client();
 944        let telemetry = self.telemetry.clone();
 945        self.status = CodegenStatus::Pending;
 946        self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
 947        self.generation = cx.spawn(|this, mut cx| async move {
 948            let model_telemetry_id = model.telemetry_id();
 949            let model_provider_id = model.provider_id();
 950            let response = model.stream_completion_text(prompt, &cx).await;
 951            let generate = async {
 952                let message_id = response
 953                    .as_ref()
 954                    .ok()
 955                    .and_then(|response| response.message_id.clone());
 956
 957                let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
 958
 959                let task = cx.background_executor().spawn({
 960                    let message_id = message_id.clone();
 961                    let executor = cx.background_executor().clone();
 962                    async move {
 963                        let mut response_latency = None;
 964                        let request_start = Instant::now();
 965                        let task = async {
 966                            let mut chunks = response?.stream;
 967                            while let Some(chunk) = chunks.next().await {
 968                                if response_latency.is_none() {
 969                                    response_latency = Some(request_start.elapsed());
 970                                }
 971                                let chunk = chunk?;
 972                                hunks_tx.send(chunk).await?;
 973                            }
 974
 975                            anyhow::Ok(())
 976                        };
 977
 978                        let result = task.await;
 979
 980                        let error_message = result.as_ref().err().map(|error| error.to_string());
 981                        report_assistant_event(
 982                            AssistantEvent {
 983                                conversation_id: None,
 984                                kind: AssistantKind::InlineTerminal,
 985                                message_id,
 986                                phase: AssistantPhase::Response,
 987                                model: model_telemetry_id,
 988                                model_provider: model_provider_id.to_string(),
 989                                response_latency,
 990                                error_message,
 991                                language_name: None,
 992                            },
 993                            telemetry,
 994                            http_client,
 995                            model_api_key,
 996                            &executor,
 997                        );
 998
 999                        result?;
1000                        anyhow::Ok(())
1001                    }
1002                });
1003
1004                this.update(&mut cx, |this, _| {
1005                    this.message_id = message_id;
1006                })?;
1007
1008                while let Some(hunk) = hunks_rx.next().await {
1009                    this.update(&mut cx, |this, cx| {
1010                        if let Some(transaction) = &mut this.transaction {
1011                            transaction.push(hunk, cx);
1012                            cx.notify();
1013                        }
1014                    })?;
1015                }
1016
1017                task.await?;
1018                anyhow::Ok(())
1019            };
1020
1021            let result = generate.await;
1022
1023            this.update(&mut cx, |this, cx| {
1024                if let Err(error) = result {
1025                    this.status = CodegenStatus::Error(error);
1026                } else {
1027                    this.status = CodegenStatus::Done;
1028                }
1029                cx.emit(CodegenEvent::Finished);
1030                cx.notify();
1031            })
1032            .ok();
1033        });
1034        cx.notify();
1035    }
1036
1037    pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
1038        self.status = CodegenStatus::Done;
1039        self.generation = Task::ready(());
1040        cx.emit(CodegenEvent::Finished);
1041        cx.notify();
1042    }
1043
1044    pub fn complete(&mut self, cx: &mut ModelContext<Self>) {
1045        if let Some(transaction) = self.transaction.take() {
1046            transaction.complete(cx);
1047        }
1048    }
1049
1050    pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
1051        if let Some(transaction) = self.transaction.take() {
1052            transaction.undo(cx);
1053        }
1054    }
1055}
1056
1057enum CodegenStatus {
1058    Idle,
1059    Pending,
1060    Done,
1061    Error(anyhow::Error),
1062}