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