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