terminal_inline_assistant.rs

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