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