terminal_inline_assistant.rs

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