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