terminal_inline_assistant.rs

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