terminal_inline_assistant.rs

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