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