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;
  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    fs: Arc<dyn Fs>,
 443    height_in_lines: u8,
 444    editor: View<Editor>,
 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(LanguageModelSelector::new(
 579                        {
 580                            let fs = self.fs.clone();
 581                            move |model, cx| {
 582                                update_settings_file::<AssistantSettings>(
 583                                    fs.clone(),
 584                                    cx,
 585                                    move |settings, _| settings.set_model(model.clone()),
 586                                );
 587                            }
 588                        },
 589                        IconButton::new("context", IconName::SettingsAlt)
 590                            .shape(IconButtonShape::Square)
 591                            .icon_size(IconSize::Small)
 592                            .icon_color(Color::Muted)
 593                            .tooltip(move |cx| {
 594                                Tooltip::with_meta(
 595                                    format!(
 596                                        "Using {}",
 597                                        LanguageModelRegistry::read_global(cx)
 598                                            .active_model()
 599                                            .map(|model| model.name().0)
 600                                            .unwrap_or_else(|| "No model selected".into()),
 601                                    ),
 602                                    None,
 603                                    "Change Model",
 604                                    cx,
 605                                )
 606                            }),
 607                    ))
 608                    .children(
 609                        if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
 610                            let error_message = SharedString::from(error.to_string());
 611                            Some(
 612                                div()
 613                                    .id("error")
 614                                    .tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
 615                                    .child(
 616                                        Icon::new(IconName::XCircle)
 617                                            .size(IconSize::Small)
 618                                            .color(Color::Error),
 619                                    ),
 620                            )
 621                        } else {
 622                            None
 623                        },
 624                    ),
 625            )
 626            .child(div().flex_1().child(self.render_prompt_editor(cx)))
 627            .child(h_flex().gap_1().pr_4().children(buttons))
 628    }
 629}
 630
 631impl FocusableView for PromptEditor {
 632    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
 633        self.editor.focus_handle(cx)
 634    }
 635}
 636
 637impl PromptEditor {
 638    const MAX_LINES: u8 = 8;
 639
 640    #[allow(clippy::too_many_arguments)]
 641    fn new(
 642        id: TerminalInlineAssistId,
 643        prompt_history: VecDeque<String>,
 644        prompt_buffer: Model<MultiBuffer>,
 645        codegen: Model<Codegen>,
 646        fs: Arc<dyn Fs>,
 647        cx: &mut ViewContext<Self>,
 648    ) -> Self {
 649        let prompt_editor = cx.new_view(|cx| {
 650            let mut editor = Editor::new(
 651                EditorMode::AutoHeight {
 652                    max_lines: Self::MAX_LINES as usize,
 653                },
 654                prompt_buffer,
 655                None,
 656                false,
 657                cx,
 658            );
 659            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
 660            editor.set_placeholder_text(Self::placeholder_text(cx), cx);
 661            editor
 662        });
 663
 664        let mut this = Self {
 665            id,
 666            height_in_lines: 1,
 667            editor: prompt_editor,
 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            fs,
 676        };
 677        this.count_lines(cx);
 678        this.subscribe_to_editor(cx);
 679        this
 680    }
 681
 682    fn placeholder_text(cx: &WindowContext) -> String {
 683        let context_keybinding = text_for_action(&crate::ToggleFocus, cx)
 684            .map(|keybinding| format!("{keybinding} for context"))
 685            .unwrap_or_default();
 686
 687        format!("Generate…{context_keybinding} ↓↑ for history")
 688    }
 689
 690    fn subscribe_to_editor(&mut self, cx: &mut ViewContext<Self>) {
 691        self.editor_subscriptions.clear();
 692        self.editor_subscriptions
 693            .push(cx.observe(&self.editor, Self::handle_prompt_editor_changed));
 694        self.editor_subscriptions
 695            .push(cx.subscribe(&self.editor, Self::handle_prompt_editor_events));
 696    }
 697
 698    fn prompt(&self, cx: &AppContext) -> String {
 699        self.editor.read(cx).text(cx)
 700    }
 701
 702    fn count_lines(&mut self, cx: &mut ViewContext<Self>) {
 703        let height_in_lines = cmp::max(
 704            2, // Make the editor at least two lines tall, to account for padding and buttons.
 705            cmp::min(
 706                self.editor
 707                    .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
 708                Self::MAX_LINES as u32,
 709            ),
 710        ) as u8;
 711
 712        if height_in_lines != self.height_in_lines {
 713            self.height_in_lines = height_in_lines;
 714            cx.emit(PromptEditorEvent::Resized { height_in_lines });
 715        }
 716    }
 717
 718    fn handle_prompt_editor_changed(&mut self, _: View<Editor>, cx: &mut ViewContext<Self>) {
 719        self.count_lines(cx);
 720    }
 721
 722    fn handle_prompt_editor_events(
 723        &mut self,
 724        _: View<Editor>,
 725        event: &EditorEvent,
 726        cx: &mut ViewContext<Self>,
 727    ) {
 728        match event {
 729            EditorEvent::Edited { .. } => {
 730                let prompt = self.editor.read(cx).text(cx);
 731                if self
 732                    .prompt_history_ix
 733                    .map_or(true, |ix| self.prompt_history[ix] != prompt)
 734                {
 735                    self.prompt_history_ix.take();
 736                    self.pending_prompt = prompt;
 737                }
 738
 739                self.edited_since_done = true;
 740                cx.notify();
 741            }
 742            _ => {}
 743        }
 744    }
 745
 746    fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
 747        match &self.codegen.read(cx).status {
 748            CodegenStatus::Idle => {
 749                self.editor
 750                    .update(cx, |editor, _| editor.set_read_only(false));
 751            }
 752            CodegenStatus::Pending => {
 753                self.editor
 754                    .update(cx, |editor, _| editor.set_read_only(true));
 755            }
 756            CodegenStatus::Done | CodegenStatus::Error(_) => {
 757                self.edited_since_done = false;
 758                self.editor
 759                    .update(cx, |editor, _| editor.set_read_only(false));
 760            }
 761        }
 762    }
 763
 764    fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
 765        match &self.codegen.read(cx).status {
 766            CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
 767                cx.emit(PromptEditorEvent::CancelRequested);
 768            }
 769            CodegenStatus::Pending => {
 770                cx.emit(PromptEditorEvent::StopRequested);
 771            }
 772        }
 773    }
 774
 775    fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
 776        match &self.codegen.read(cx).status {
 777            CodegenStatus::Idle => {
 778                if !self.editor.read(cx).text(cx).trim().is_empty() {
 779                    cx.emit(PromptEditorEvent::StartRequested);
 780                }
 781            }
 782            CodegenStatus::Pending => {
 783                cx.emit(PromptEditorEvent::DismissRequested);
 784            }
 785            CodegenStatus::Done => {
 786                if self.edited_since_done {
 787                    cx.emit(PromptEditorEvent::StartRequested);
 788                } else {
 789                    cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
 790                }
 791            }
 792            CodegenStatus::Error(_) => {
 793                cx.emit(PromptEditorEvent::StartRequested);
 794            }
 795        }
 796    }
 797
 798    fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
 799        if matches!(self.codegen.read(cx).status, CodegenStatus::Done) {
 800            cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
 801        }
 802    }
 803
 804    fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
 805        if let Some(ix) = self.prompt_history_ix {
 806            if ix > 0 {
 807                self.prompt_history_ix = Some(ix - 1);
 808                let prompt = self.prompt_history[ix - 1].as_str();
 809                self.editor.update(cx, |editor, cx| {
 810                    editor.set_text(prompt, cx);
 811                    editor.move_to_beginning(&Default::default(), cx);
 812                });
 813            }
 814        } else if !self.prompt_history.is_empty() {
 815            self.prompt_history_ix = Some(self.prompt_history.len() - 1);
 816            let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
 817            self.editor.update(cx, |editor, cx| {
 818                editor.set_text(prompt, cx);
 819                editor.move_to_beginning(&Default::default(), cx);
 820            });
 821        }
 822    }
 823
 824    fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
 825        if let Some(ix) = self.prompt_history_ix {
 826            if ix < self.prompt_history.len() - 1 {
 827                self.prompt_history_ix = Some(ix + 1);
 828                let prompt = self.prompt_history[ix + 1].as_str();
 829                self.editor.update(cx, |editor, cx| {
 830                    editor.set_text(prompt, cx);
 831                    editor.move_to_end(&Default::default(), cx)
 832                });
 833            } else {
 834                self.prompt_history_ix = None;
 835                let prompt = self.pending_prompt.as_str();
 836                self.editor.update(cx, |editor, cx| {
 837                    editor.set_text(prompt, cx);
 838                    editor.move_to_end(&Default::default(), cx)
 839                });
 840            }
 841        }
 842    }
 843
 844    fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 845        let settings = ThemeSettings::get_global(cx);
 846        let text_style = TextStyle {
 847            color: if self.editor.read(cx).read_only(cx) {
 848                cx.theme().colors().text_disabled
 849            } else {
 850                cx.theme().colors().text
 851            },
 852            font_family: settings.buffer_font.family.clone(),
 853            font_fallbacks: settings.buffer_font.fallbacks.clone(),
 854            font_size: settings.buffer_font_size.into(),
 855            font_weight: settings.buffer_font.weight,
 856            line_height: relative(settings.buffer_line_height.value()),
 857            ..Default::default()
 858        };
 859        EditorElement::new(
 860            &self.editor,
 861            EditorStyle {
 862                background: cx.theme().colors().editor_background,
 863                local_player: cx.theme().players().local(),
 864                text: text_style,
 865                ..Default::default()
 866            },
 867        )
 868    }
 869}
 870
 871#[derive(Debug)]
 872pub enum CodegenEvent {
 873    Finished,
 874}
 875
 876impl EventEmitter<CodegenEvent> for Codegen {}
 877
 878const CLEAR_INPUT: &str = "\x15";
 879const CARRIAGE_RETURN: &str = "\x0d";
 880
 881struct TerminalTransaction {
 882    terminal: Model<Terminal>,
 883}
 884
 885impl TerminalTransaction {
 886    pub fn start(terminal: Model<Terminal>) -> Self {
 887        Self { terminal }
 888    }
 889
 890    pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
 891        // Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
 892        let input = Self::sanitize_input(hunk);
 893        self.terminal
 894            .update(cx, |terminal, _| terminal.input(input));
 895    }
 896
 897    pub fn undo(&self, cx: &mut AppContext) {
 898        self.terminal
 899            .update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
 900    }
 901
 902    pub fn complete(&self, cx: &mut AppContext) {
 903        self.terminal.update(cx, |terminal, _| {
 904            terminal.input(CARRIAGE_RETURN.to_string())
 905        });
 906    }
 907
 908    fn sanitize_input(input: String) -> String {
 909        input.replace(['\r', '\n'], "")
 910    }
 911}
 912
 913pub struct Codegen {
 914    status: CodegenStatus,
 915    telemetry: Option<Arc<Telemetry>>,
 916    terminal: Model<Terminal>,
 917    generation: Task<()>,
 918    message_id: Option<String>,
 919    transaction: Option<TerminalTransaction>,
 920}
 921
 922impl Codegen {
 923    pub fn new(terminal: Model<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
 924        Self {
 925            terminal,
 926            telemetry,
 927            status: CodegenStatus::Idle,
 928            generation: Task::ready(()),
 929            message_id: None,
 930            transaction: None,
 931        }
 932    }
 933
 934    pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
 935        let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
 936            return;
 937        };
 938
 939        let model_api_key = model.api_key(cx);
 940        let http_client = cx.http_client();
 941        let telemetry = self.telemetry.clone();
 942        self.status = CodegenStatus::Pending;
 943        self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
 944        self.generation = cx.spawn(|this, mut cx| async move {
 945            let model_telemetry_id = model.telemetry_id();
 946            let model_provider_id = model.provider_id();
 947            let response = model.stream_completion_text(prompt, &cx).await;
 948            let generate = async {
 949                let message_id = response
 950                    .as_ref()
 951                    .ok()
 952                    .and_then(|response| response.message_id.clone());
 953
 954                let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
 955
 956                let task = cx.background_executor().spawn({
 957                    let message_id = message_id.clone();
 958                    let executor = cx.background_executor().clone();
 959                    async move {
 960                        let mut response_latency = None;
 961                        let request_start = Instant::now();
 962                        let task = async {
 963                            let mut chunks = response?.stream;
 964                            while let Some(chunk) = chunks.next().await {
 965                                if response_latency.is_none() {
 966                                    response_latency = Some(request_start.elapsed());
 967                                }
 968                                let chunk = chunk?;
 969                                hunks_tx.send(chunk).await?;
 970                            }
 971
 972                            anyhow::Ok(())
 973                        };
 974
 975                        let result = task.await;
 976
 977                        let error_message = result.as_ref().err().map(|error| error.to_string());
 978                        report_assistant_event(
 979                            AssistantEvent {
 980                                conversation_id: None,
 981                                kind: AssistantKind::InlineTerminal,
 982                                message_id,
 983                                phase: AssistantPhase::Response,
 984                                model: model_telemetry_id,
 985                                model_provider: model_provider_id.to_string(),
 986                                response_latency,
 987                                error_message,
 988                                language_name: None,
 989                            },
 990                            telemetry,
 991                            http_client,
 992                            model_api_key,
 993                            &executor,
 994                        );
 995
 996                        result?;
 997                        anyhow::Ok(())
 998                    }
 999                });
1000
1001                this.update(&mut cx, |this, _| {
1002                    this.message_id = message_id;
1003                })?;
1004
1005                while let Some(hunk) = hunks_rx.next().await {
1006                    this.update(&mut cx, |this, cx| {
1007                        if let Some(transaction) = &mut this.transaction {
1008                            transaction.push(hunk, cx);
1009                            cx.notify();
1010                        }
1011                    })?;
1012                }
1013
1014                task.await?;
1015                anyhow::Ok(())
1016            };
1017
1018            let result = generate.await;
1019
1020            this.update(&mut cx, |this, cx| {
1021                if let Err(error) = result {
1022                    this.status = CodegenStatus::Error(error);
1023                } else {
1024                    this.status = CodegenStatus::Done;
1025                }
1026                cx.emit(CodegenEvent::Finished);
1027                cx.notify();
1028            })
1029            .ok();
1030        });
1031        cx.notify();
1032    }
1033
1034    pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
1035        self.status = CodegenStatus::Done;
1036        self.generation = Task::ready(());
1037        cx.emit(CodegenEvent::Finished);
1038        cx.notify();
1039    }
1040
1041    pub fn complete(&mut self, cx: &mut ModelContext<Self>) {
1042        if let Some(transaction) = self.transaction.take() {
1043            transaction.complete(cx);
1044        }
1045    }
1046
1047    pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
1048        if let Some(transaction) = self.transaction.take() {
1049            transaction.undo(cx);
1050        }
1051    }
1052}
1053
1054enum CodegenStatus {
1055    Idle,
1056    Pending,
1057    Done,
1058    Error(anyhow::Error),
1059}