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: 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: 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: 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.workspace.upgrade() {
400 let error =
401 format!("Terminal inline assistant error: {}", error);
402 workspace.update(cx, |workspace, cx| {
403 struct InlineAssistantError;
404
405 let id =
406 NotificationId::composite::<InlineAssistantError>(
407 assist_id.0,
408 );
409
410 workspace.show_toast(Toast::new(id, error), cx);
411 })
412 }
413 }
414 }
415
416 if assist.prompt_editor.is_none() {
417 this.finish_assist(assist_id, false, false, cx);
418 }
419 }
420 })
421 }),
422 ],
423 }
424 }
425}
426
427enum PromptEditorEvent {
428 StartRequested,
429 StopRequested,
430 ConfirmRequested { execute: bool },
431 CancelRequested,
432 DismissRequested,
433 Resized { height_in_lines: u8 },
434}
435
436struct PromptEditor {
437 id: TerminalInlineAssistId,
438 height_in_lines: u8,
439 editor: View<Editor>,
440 language_model_selector: View<LanguageModelSelector>,
441 edited_since_done: bool,
442 prompt_history: VecDeque<String>,
443 prompt_history_ix: Option<usize>,
444 pending_prompt: String,
445 codegen: Model<Codegen>,
446 _codegen_subscription: Subscription,
447 editor_subscriptions: Vec<Subscription>,
448}
449
450impl EventEmitter<PromptEditorEvent> for PromptEditor {}
451
452impl Render for PromptEditor {
453 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
454 let status = &self.codegen.read(cx).status;
455 let mut buttons = vec![Button::new("add-context", "Add Context")
456 .style(ButtonStyle::Filled)
457 .icon(IconName::Plus)
458 .icon_position(IconPosition::Start)
459 .into_any_element()];
460
461 buttons.extend(match status {
462 CodegenStatus::Idle => vec![
463 IconButton::new("cancel", IconName::Close)
464 .icon_color(Color::Muted)
465 .shape(IconButtonShape::Square)
466 .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
467 .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
468 .into_any_element(),
469 IconButton::new("start", IconName::SparkleAlt)
470 .icon_color(Color::Muted)
471 .shape(IconButtonShape::Square)
472 .tooltip(|cx| Tooltip::for_action("Generate", &menu::Confirm, cx))
473 .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)))
474 .into_any_element(),
475 ],
476 CodegenStatus::Pending => vec![
477 IconButton::new("cancel", IconName::Close)
478 .icon_color(Color::Muted)
479 .shape(IconButtonShape::Square)
480 .tooltip(|cx| Tooltip::text("Cancel Assist", cx))
481 .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
482 .into_any_element(),
483 IconButton::new("stop", IconName::Stop)
484 .icon_color(Color::Error)
485 .shape(IconButtonShape::Square)
486 .tooltip(|cx| {
487 Tooltip::with_meta(
488 "Interrupt Generation",
489 Some(&menu::Cancel),
490 "Changes won't be discarded",
491 cx,
492 )
493 })
494 .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
495 .into_any_element(),
496 ],
497 CodegenStatus::Error(_) | CodegenStatus::Done => {
498 let cancel = IconButton::new("cancel", IconName::Close)
499 .icon_color(Color::Muted)
500 .shape(IconButtonShape::Square)
501 .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
502 .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
503 .into_any_element();
504
505 let has_error = matches!(status, CodegenStatus::Error(_));
506 if has_error || self.edited_since_done {
507 vec![
508 cancel,
509 IconButton::new("restart", IconName::RotateCw)
510 .icon_color(Color::Info)
511 .shape(IconButtonShape::Square)
512 .tooltip(|cx| {
513 Tooltip::with_meta(
514 "Restart Generation",
515 Some(&menu::Confirm),
516 "Changes will be discarded",
517 cx,
518 )
519 })
520 .on_click(cx.listener(|_, _, cx| {
521 cx.emit(PromptEditorEvent::StartRequested);
522 }))
523 .into_any_element(),
524 ]
525 } else {
526 vec![
527 cancel,
528 IconButton::new("accept", IconName::Check)
529 .icon_color(Color::Info)
530 .shape(IconButtonShape::Square)
531 .tooltip(|cx| {
532 Tooltip::for_action("Accept Generated Command", &menu::Confirm, cx)
533 })
534 .on_click(cx.listener(|_, _, cx| {
535 cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
536 }))
537 .into_any_element(),
538 IconButton::new("confirm", IconName::Play)
539 .icon_color(Color::Info)
540 .shape(IconButtonShape::Square)
541 .tooltip(|cx| {
542 Tooltip::for_action(
543 "Execute Generated Command",
544 &menu::SecondaryConfirm,
545 cx,
546 )
547 })
548 .on_click(cx.listener(|_, _, cx| {
549 cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
550 }))
551 .into_any_element(),
552 ]
553 }
554 }
555 });
556
557 h_flex()
558 .bg(cx.theme().colors().editor_background)
559 .border_y_1()
560 .border_color(cx.theme().status().info_border)
561 .py_2()
562 .h_full()
563 .w_full()
564 .on_action(cx.listener(Self::confirm))
565 .on_action(cx.listener(Self::secondary_confirm))
566 .on_action(cx.listener(Self::cancel))
567 .on_action(cx.listener(Self::move_up))
568 .on_action(cx.listener(Self::move_down))
569 .child(
570 h_flex()
571 .w_12()
572 .justify_center()
573 .gap_2()
574 .child(LanguageModelSelectorPopoverMenu::new(
575 self.language_model_selector.clone(),
576 IconButton::new("context", IconName::SettingsAlt)
577 .shape(IconButtonShape::Square)
578 .icon_size(IconSize::Small)
579 .icon_color(Color::Muted)
580 .tooltip(move |cx| {
581 Tooltip::with_meta(
582 format!(
583 "Using {}",
584 LanguageModelRegistry::read_global(cx)
585 .active_model()
586 .map(|model| model.name().0)
587 .unwrap_or_else(|| "No model selected".into()),
588 ),
589 None,
590 "Change Model",
591 cx,
592 )
593 }),
594 ))
595 .children(
596 if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
597 let error_message = SharedString::from(error.to_string());
598 Some(
599 div()
600 .id("error")
601 .tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
602 .child(
603 Icon::new(IconName::XCircle)
604 .size(IconSize::Small)
605 .color(Color::Error),
606 ),
607 )
608 } else {
609 None
610 },
611 ),
612 )
613 .child(div().flex_1().child(self.render_prompt_editor(cx)))
614 .child(h_flex().gap_1().pr_4().children(buttons))
615 }
616}
617
618impl FocusableView for PromptEditor {
619 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
620 self.editor.focus_handle(cx)
621 }
622}
623
624impl PromptEditor {
625 const MAX_LINES: u8 = 8;
626
627 #[allow(clippy::too_many_arguments)]
628 fn new(
629 id: TerminalInlineAssistId,
630 prompt_history: VecDeque<String>,
631 prompt_buffer: Model<MultiBuffer>,
632 codegen: Model<Codegen>,
633 fs: Arc<dyn Fs>,
634 cx: &mut ViewContext<Self>,
635 ) -> Self {
636 let prompt_editor = cx.new_view(|cx| {
637 let mut editor = Editor::new(
638 EditorMode::AutoHeight {
639 max_lines: Self::MAX_LINES as usize,
640 },
641 prompt_buffer,
642 None,
643 false,
644 cx,
645 );
646 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
647 editor.set_placeholder_text(Self::placeholder_text(cx), cx);
648 editor
649 });
650
651 let mut this = Self {
652 id,
653 height_in_lines: 1,
654 editor: prompt_editor,
655 language_model_selector: cx.new_view(|cx| {
656 let fs = fs.clone();
657 LanguageModelSelector::new(
658 move |model, cx| {
659 update_settings_file::<AssistantSettings>(
660 fs.clone(),
661 cx,
662 move |settings, _| settings.set_model(model.clone()),
663 );
664 },
665 cx,
666 )
667 }),
668 edited_since_done: false,
669 prompt_history,
670 prompt_history_ix: None,
671 pending_prompt: String::new(),
672 _codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
673 editor_subscriptions: Vec::new(),
674 codegen,
675 };
676 this.count_lines(cx);
677 this.subscribe_to_editor(cx);
678 this
679 }
680
681 fn placeholder_text(cx: &WindowContext) -> String {
682 let context_keybinding = text_for_action(&crate::ToggleFocus, cx)
683 .map(|keybinding| format!(" • {keybinding} for context"))
684 .unwrap_or_default();
685
686 format!("Generate…{context_keybinding} ↓↑ for history")
687 }
688
689 fn subscribe_to_editor(&mut self, cx: &mut ViewContext<Self>) {
690 self.editor_subscriptions.clear();
691 self.editor_subscriptions
692 .push(cx.observe(&self.editor, Self::handle_prompt_editor_changed));
693 self.editor_subscriptions
694 .push(cx.subscribe(&self.editor, Self::handle_prompt_editor_events));
695 }
696
697 fn prompt(&self, cx: &AppContext) -> String {
698 self.editor.read(cx).text(cx)
699 }
700
701 fn count_lines(&mut self, cx: &mut ViewContext<Self>) {
702 let height_in_lines = cmp::max(
703 2, // Make the editor at least two lines tall, to account for padding and buttons.
704 cmp::min(
705 self.editor
706 .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
707 Self::MAX_LINES as u32,
708 ),
709 ) as u8;
710
711 if height_in_lines != self.height_in_lines {
712 self.height_in_lines = height_in_lines;
713 cx.emit(PromptEditorEvent::Resized { height_in_lines });
714 }
715 }
716
717 fn handle_prompt_editor_changed(&mut self, _: View<Editor>, cx: &mut ViewContext<Self>) {
718 self.count_lines(cx);
719 }
720
721 fn handle_prompt_editor_events(
722 &mut self,
723 _: View<Editor>,
724 event: &EditorEvent,
725 cx: &mut ViewContext<Self>,
726 ) {
727 match event {
728 EditorEvent::Edited { .. } => {
729 let prompt = self.editor.read(cx).text(cx);
730 if self
731 .prompt_history_ix
732 .map_or(true, |ix| self.prompt_history[ix] != prompt)
733 {
734 self.prompt_history_ix.take();
735 self.pending_prompt = prompt;
736 }
737
738 self.edited_since_done = true;
739 cx.notify();
740 }
741 _ => {}
742 }
743 }
744
745 fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
746 match &self.codegen.read(cx).status {
747 CodegenStatus::Idle => {
748 self.editor
749 .update(cx, |editor, _| editor.set_read_only(false));
750 }
751 CodegenStatus::Pending => {
752 self.editor
753 .update(cx, |editor, _| editor.set_read_only(true));
754 }
755 CodegenStatus::Done | CodegenStatus::Error(_) => {
756 self.edited_since_done = false;
757 self.editor
758 .update(cx, |editor, _| editor.set_read_only(false));
759 }
760 }
761 }
762
763 fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
764 match &self.codegen.read(cx).status {
765 CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
766 cx.emit(PromptEditorEvent::CancelRequested);
767 }
768 CodegenStatus::Pending => {
769 cx.emit(PromptEditorEvent::StopRequested);
770 }
771 }
772 }
773
774 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
775 match &self.codegen.read(cx).status {
776 CodegenStatus::Idle => {
777 if !self.editor.read(cx).text(cx).trim().is_empty() {
778 cx.emit(PromptEditorEvent::StartRequested);
779 }
780 }
781 CodegenStatus::Pending => {
782 cx.emit(PromptEditorEvent::DismissRequested);
783 }
784 CodegenStatus::Done => {
785 if self.edited_since_done {
786 cx.emit(PromptEditorEvent::StartRequested);
787 } else {
788 cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
789 }
790 }
791 CodegenStatus::Error(_) => {
792 cx.emit(PromptEditorEvent::StartRequested);
793 }
794 }
795 }
796
797 fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
798 if matches!(self.codegen.read(cx).status, CodegenStatus::Done) {
799 cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
800 }
801 }
802
803 fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
804 if let Some(ix) = self.prompt_history_ix {
805 if ix > 0 {
806 self.prompt_history_ix = Some(ix - 1);
807 let prompt = self.prompt_history[ix - 1].as_str();
808 self.editor.update(cx, |editor, cx| {
809 editor.set_text(prompt, cx);
810 editor.move_to_beginning(&Default::default(), cx);
811 });
812 }
813 } else if !self.prompt_history.is_empty() {
814 self.prompt_history_ix = Some(self.prompt_history.len() - 1);
815 let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
816 self.editor.update(cx, |editor, cx| {
817 editor.set_text(prompt, cx);
818 editor.move_to_beginning(&Default::default(), cx);
819 });
820 }
821 }
822
823 fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
824 if let Some(ix) = self.prompt_history_ix {
825 if ix < self.prompt_history.len() - 1 {
826 self.prompt_history_ix = Some(ix + 1);
827 let prompt = self.prompt_history[ix + 1].as_str();
828 self.editor.update(cx, |editor, cx| {
829 editor.set_text(prompt, cx);
830 editor.move_to_end(&Default::default(), cx)
831 });
832 } else {
833 self.prompt_history_ix = None;
834 let prompt = self.pending_prompt.as_str();
835 self.editor.update(cx, |editor, cx| {
836 editor.set_text(prompt, cx);
837 editor.move_to_end(&Default::default(), cx)
838 });
839 }
840 }
841 }
842
843 fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
844 let settings = ThemeSettings::get_global(cx);
845 let text_style = TextStyle {
846 color: if self.editor.read(cx).read_only(cx) {
847 cx.theme().colors().text_disabled
848 } else {
849 cx.theme().colors().text
850 },
851 font_family: settings.buffer_font.family.clone(),
852 font_fallbacks: settings.buffer_font.fallbacks.clone(),
853 font_size: settings.buffer_font_size.into(),
854 font_weight: settings.buffer_font.weight,
855 line_height: relative(settings.buffer_line_height.value()),
856 ..Default::default()
857 };
858 EditorElement::new(
859 &self.editor,
860 EditorStyle {
861 background: cx.theme().colors().editor_background,
862 local_player: cx.theme().players().local(),
863 text: text_style,
864 ..Default::default()
865 },
866 )
867 }
868}
869
870#[derive(Debug)]
871pub enum CodegenEvent {
872 Finished,
873}
874
875impl EventEmitter<CodegenEvent> for Codegen {}
876
877const CLEAR_INPUT: &str = "\x15";
878const CARRIAGE_RETURN: &str = "\x0d";
879
880struct TerminalTransaction {
881 terminal: Model<Terminal>,
882}
883
884impl TerminalTransaction {
885 pub fn start(terminal: Model<Terminal>) -> Self {
886 Self { terminal }
887 }
888
889 pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
890 // Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
891 let input = Self::sanitize_input(hunk);
892 self.terminal
893 .update(cx, |terminal, _| terminal.input(input));
894 }
895
896 pub fn undo(&self, cx: &mut AppContext) {
897 self.terminal
898 .update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
899 }
900
901 pub fn complete(&self, cx: &mut AppContext) {
902 self.terminal.update(cx, |terminal, _| {
903 terminal.input(CARRIAGE_RETURN.to_string())
904 });
905 }
906
907 fn sanitize_input(input: String) -> String {
908 input.replace(['\r', '\n'], "")
909 }
910}
911
912pub struct Codegen {
913 status: CodegenStatus,
914 telemetry: Option<Arc<Telemetry>>,
915 terminal: Model<Terminal>,
916 generation: Task<()>,
917 message_id: Option<String>,
918 transaction: Option<TerminalTransaction>,
919}
920
921impl Codegen {
922 pub fn new(terminal: Model<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
923 Self {
924 terminal,
925 telemetry,
926 status: CodegenStatus::Idle,
927 generation: Task::ready(()),
928 message_id: None,
929 transaction: None,
930 }
931 }
932
933 pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
934 let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
935 return;
936 };
937
938 let model_api_key = model.api_key(cx);
939 let http_client = cx.http_client();
940 let telemetry = self.telemetry.clone();
941 self.status = CodegenStatus::Pending;
942 self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
943 self.generation = cx.spawn(|this, mut cx| async move {
944 let model_telemetry_id = model.telemetry_id();
945 let model_provider_id = model.provider_id();
946 let response = model.stream_completion_text(prompt, &cx).await;
947 let generate = async {
948 let message_id = response
949 .as_ref()
950 .ok()
951 .and_then(|response| response.message_id.clone());
952
953 let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
954
955 let task = cx.background_executor().spawn({
956 let message_id = message_id.clone();
957 let executor = cx.background_executor().clone();
958 async move {
959 let mut response_latency = None;
960 let request_start = Instant::now();
961 let task = async {
962 let mut chunks = response?.stream;
963 while let Some(chunk) = chunks.next().await {
964 if response_latency.is_none() {
965 response_latency = Some(request_start.elapsed());
966 }
967 let chunk = chunk?;
968 hunks_tx.send(chunk).await?;
969 }
970
971 anyhow::Ok(())
972 };
973
974 let result = task.await;
975
976 let error_message = result.as_ref().err().map(|error| error.to_string());
977 report_assistant_event(
978 AssistantEvent {
979 conversation_id: None,
980 kind: AssistantKind::InlineTerminal,
981 message_id,
982 phase: AssistantPhase::Response,
983 model: model_telemetry_id,
984 model_provider: model_provider_id.to_string(),
985 response_latency,
986 error_message,
987 language_name: None,
988 },
989 telemetry,
990 http_client,
991 model_api_key,
992 &executor,
993 );
994
995 result?;
996 anyhow::Ok(())
997 }
998 });
999
1000 this.update(&mut cx, |this, _| {
1001 this.message_id = message_id;
1002 })?;
1003
1004 while let Some(hunk) = hunks_rx.next().await {
1005 this.update(&mut cx, |this, cx| {
1006 if let Some(transaction) = &mut this.transaction {
1007 transaction.push(hunk, cx);
1008 cx.notify();
1009 }
1010 })?;
1011 }
1012
1013 task.await?;
1014 anyhow::Ok(())
1015 };
1016
1017 let result = generate.await;
1018
1019 this.update(&mut cx, |this, cx| {
1020 if let Err(error) = result {
1021 this.status = CodegenStatus::Error(error);
1022 } else {
1023 this.status = CodegenStatus::Done;
1024 }
1025 cx.emit(CodegenEvent::Finished);
1026 cx.notify();
1027 })
1028 .ok();
1029 });
1030 cx.notify();
1031 }
1032
1033 pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
1034 self.status = CodegenStatus::Done;
1035 self.generation = Task::ready(());
1036 cx.emit(CodegenEvent::Finished);
1037 cx.notify();
1038 }
1039
1040 pub fn complete(&mut self, cx: &mut ModelContext<Self>) {
1041 if let Some(transaction) = self.transaction.take() {
1042 transaction.complete(cx);
1043 }
1044 }
1045
1046 pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
1047 if let Some(transaction) = self.transaction.take() {
1048 transaction.undo(cx);
1049 }
1050 }
1051}
1052
1053enum CodegenStatus {
1054 Idle,
1055 Pending,
1056 Done,
1057 Error(anyhow::Error),
1058}