terminal_inline_assistant.rs

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