terminal_inline_assistant.rs

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