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;
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 fs: Arc<dyn Fs>,
443 height_in_lines: u8,
444 editor: View<Editor>,
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(LanguageModelSelector::new(
579 {
580 let fs = self.fs.clone();
581 move |model, cx| {
582 update_settings_file::<AssistantSettings>(
583 fs.clone(),
584 cx,
585 move |settings, _| settings.set_model(model.clone()),
586 );
587 }
588 },
589 IconButton::new("context", IconName::SettingsAlt)
590 .shape(IconButtonShape::Square)
591 .icon_size(IconSize::Small)
592 .icon_color(Color::Muted)
593 .tooltip(move |cx| {
594 Tooltip::with_meta(
595 format!(
596 "Using {}",
597 LanguageModelRegistry::read_global(cx)
598 .active_model()
599 .map(|model| model.name().0)
600 .unwrap_or_else(|| "No model selected".into()),
601 ),
602 None,
603 "Change Model",
604 cx,
605 )
606 }),
607 ))
608 .children(
609 if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
610 let error_message = SharedString::from(error.to_string());
611 Some(
612 div()
613 .id("error")
614 .tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
615 .child(
616 Icon::new(IconName::XCircle)
617 .size(IconSize::Small)
618 .color(Color::Error),
619 ),
620 )
621 } else {
622 None
623 },
624 ),
625 )
626 .child(div().flex_1().child(self.render_prompt_editor(cx)))
627 .child(h_flex().gap_1().pr_4().children(buttons))
628 }
629}
630
631impl FocusableView for PromptEditor {
632 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
633 self.editor.focus_handle(cx)
634 }
635}
636
637impl PromptEditor {
638 const MAX_LINES: u8 = 8;
639
640 #[allow(clippy::too_many_arguments)]
641 fn new(
642 id: TerminalInlineAssistId,
643 prompt_history: VecDeque<String>,
644 prompt_buffer: Model<MultiBuffer>,
645 codegen: Model<Codegen>,
646 fs: Arc<dyn Fs>,
647 cx: &mut ViewContext<Self>,
648 ) -> Self {
649 let prompt_editor = cx.new_view(|cx| {
650 let mut editor = Editor::new(
651 EditorMode::AutoHeight {
652 max_lines: Self::MAX_LINES as usize,
653 },
654 prompt_buffer,
655 None,
656 false,
657 cx,
658 );
659 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
660 editor.set_placeholder_text(Self::placeholder_text(cx), cx);
661 editor
662 });
663
664 let mut this = Self {
665 id,
666 height_in_lines: 1,
667 editor: prompt_editor,
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 fs,
676 };
677 this.count_lines(cx);
678 this.subscribe_to_editor(cx);
679 this
680 }
681
682 fn placeholder_text(cx: &WindowContext) -> String {
683 let context_keybinding = text_for_action(&crate::ToggleFocus, cx)
684 .map(|keybinding| format!(" • {keybinding} for context"))
685 .unwrap_or_default();
686
687 format!("Generate…{context_keybinding} ↓↑ for history")
688 }
689
690 fn subscribe_to_editor(&mut self, cx: &mut ViewContext<Self>) {
691 self.editor_subscriptions.clear();
692 self.editor_subscriptions
693 .push(cx.observe(&self.editor, Self::handle_prompt_editor_changed));
694 self.editor_subscriptions
695 .push(cx.subscribe(&self.editor, Self::handle_prompt_editor_events));
696 }
697
698 fn prompt(&self, cx: &AppContext) -> String {
699 self.editor.read(cx).text(cx)
700 }
701
702 fn count_lines(&mut self, cx: &mut ViewContext<Self>) {
703 let height_in_lines = cmp::max(
704 2, // Make the editor at least two lines tall, to account for padding and buttons.
705 cmp::min(
706 self.editor
707 .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
708 Self::MAX_LINES as u32,
709 ),
710 ) as u8;
711
712 if height_in_lines != self.height_in_lines {
713 self.height_in_lines = height_in_lines;
714 cx.emit(PromptEditorEvent::Resized { height_in_lines });
715 }
716 }
717
718 fn handle_prompt_editor_changed(&mut self, _: View<Editor>, cx: &mut ViewContext<Self>) {
719 self.count_lines(cx);
720 }
721
722 fn handle_prompt_editor_events(
723 &mut self,
724 _: View<Editor>,
725 event: &EditorEvent,
726 cx: &mut ViewContext<Self>,
727 ) {
728 match event {
729 EditorEvent::Edited { .. } => {
730 let prompt = self.editor.read(cx).text(cx);
731 if self
732 .prompt_history_ix
733 .map_or(true, |ix| self.prompt_history[ix] != prompt)
734 {
735 self.prompt_history_ix.take();
736 self.pending_prompt = prompt;
737 }
738
739 self.edited_since_done = true;
740 cx.notify();
741 }
742 _ => {}
743 }
744 }
745
746 fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
747 match &self.codegen.read(cx).status {
748 CodegenStatus::Idle => {
749 self.editor
750 .update(cx, |editor, _| editor.set_read_only(false));
751 }
752 CodegenStatus::Pending => {
753 self.editor
754 .update(cx, |editor, _| editor.set_read_only(true));
755 }
756 CodegenStatus::Done | CodegenStatus::Error(_) => {
757 self.edited_since_done = false;
758 self.editor
759 .update(cx, |editor, _| editor.set_read_only(false));
760 }
761 }
762 }
763
764 fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
765 match &self.codegen.read(cx).status {
766 CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
767 cx.emit(PromptEditorEvent::CancelRequested);
768 }
769 CodegenStatus::Pending => {
770 cx.emit(PromptEditorEvent::StopRequested);
771 }
772 }
773 }
774
775 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
776 match &self.codegen.read(cx).status {
777 CodegenStatus::Idle => {
778 if !self.editor.read(cx).text(cx).trim().is_empty() {
779 cx.emit(PromptEditorEvent::StartRequested);
780 }
781 }
782 CodegenStatus::Pending => {
783 cx.emit(PromptEditorEvent::DismissRequested);
784 }
785 CodegenStatus::Done => {
786 if self.edited_since_done {
787 cx.emit(PromptEditorEvent::StartRequested);
788 } else {
789 cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
790 }
791 }
792 CodegenStatus::Error(_) => {
793 cx.emit(PromptEditorEvent::StartRequested);
794 }
795 }
796 }
797
798 fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
799 if matches!(self.codegen.read(cx).status, CodegenStatus::Done) {
800 cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
801 }
802 }
803
804 fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
805 if let Some(ix) = self.prompt_history_ix {
806 if ix > 0 {
807 self.prompt_history_ix = Some(ix - 1);
808 let prompt = self.prompt_history[ix - 1].as_str();
809 self.editor.update(cx, |editor, cx| {
810 editor.set_text(prompt, cx);
811 editor.move_to_beginning(&Default::default(), cx);
812 });
813 }
814 } else if !self.prompt_history.is_empty() {
815 self.prompt_history_ix = Some(self.prompt_history.len() - 1);
816 let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
817 self.editor.update(cx, |editor, cx| {
818 editor.set_text(prompt, cx);
819 editor.move_to_beginning(&Default::default(), cx);
820 });
821 }
822 }
823
824 fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
825 if let Some(ix) = self.prompt_history_ix {
826 if ix < self.prompt_history.len() - 1 {
827 self.prompt_history_ix = Some(ix + 1);
828 let prompt = self.prompt_history[ix + 1].as_str();
829 self.editor.update(cx, |editor, cx| {
830 editor.set_text(prompt, cx);
831 editor.move_to_end(&Default::default(), cx)
832 });
833 } else {
834 self.prompt_history_ix = None;
835 let prompt = self.pending_prompt.as_str();
836 self.editor.update(cx, |editor, cx| {
837 editor.set_text(prompt, cx);
838 editor.move_to_end(&Default::default(), cx)
839 });
840 }
841 }
842 }
843
844 fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
845 let settings = ThemeSettings::get_global(cx);
846 let text_style = TextStyle {
847 color: if self.editor.read(cx).read_only(cx) {
848 cx.theme().colors().text_disabled
849 } else {
850 cx.theme().colors().text
851 },
852 font_family: settings.buffer_font.family.clone(),
853 font_fallbacks: settings.buffer_font.fallbacks.clone(),
854 font_size: settings.buffer_font_size.into(),
855 font_weight: settings.buffer_font.weight,
856 line_height: relative(settings.buffer_line_height.value()),
857 ..Default::default()
858 };
859 EditorElement::new(
860 &self.editor,
861 EditorStyle {
862 background: cx.theme().colors().editor_background,
863 local_player: cx.theme().players().local(),
864 text: text_style,
865 ..Default::default()
866 },
867 )
868 }
869}
870
871#[derive(Debug)]
872pub enum CodegenEvent {
873 Finished,
874}
875
876impl EventEmitter<CodegenEvent> for Codegen {}
877
878const CLEAR_INPUT: &str = "\x15";
879const CARRIAGE_RETURN: &str = "\x0d";
880
881struct TerminalTransaction {
882 terminal: Model<Terminal>,
883}
884
885impl TerminalTransaction {
886 pub fn start(terminal: Model<Terminal>) -> Self {
887 Self { terminal }
888 }
889
890 pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
891 // Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
892 let input = Self::sanitize_input(hunk);
893 self.terminal
894 .update(cx, |terminal, _| terminal.input(input));
895 }
896
897 pub fn undo(&self, cx: &mut AppContext) {
898 self.terminal
899 .update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
900 }
901
902 pub fn complete(&self, cx: &mut AppContext) {
903 self.terminal.update(cx, |terminal, _| {
904 terminal.input(CARRIAGE_RETURN.to_string())
905 });
906 }
907
908 fn sanitize_input(input: String) -> String {
909 input.replace(['\r', '\n'], "")
910 }
911}
912
913pub struct Codegen {
914 status: CodegenStatus,
915 telemetry: Option<Arc<Telemetry>>,
916 terminal: Model<Terminal>,
917 generation: Task<()>,
918 message_id: Option<String>,
919 transaction: Option<TerminalTransaction>,
920}
921
922impl Codegen {
923 pub fn new(terminal: Model<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
924 Self {
925 terminal,
926 telemetry,
927 status: CodegenStatus::Idle,
928 generation: Task::ready(()),
929 message_id: None,
930 transaction: None,
931 }
932 }
933
934 pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
935 let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
936 return;
937 };
938
939 let model_api_key = model.api_key(cx);
940 let http_client = cx.http_client();
941 let telemetry = self.telemetry.clone();
942 self.status = CodegenStatus::Pending;
943 self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
944 self.generation = cx.spawn(|this, mut cx| async move {
945 let model_telemetry_id = model.telemetry_id();
946 let model_provider_id = model.provider_id();
947 let response = model.stream_completion_text(prompt, &cx).await;
948 let generate = async {
949 let message_id = response
950 .as_ref()
951 .ok()
952 .and_then(|response| response.message_id.clone());
953
954 let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
955
956 let task = cx.background_executor().spawn({
957 let message_id = message_id.clone();
958 let executor = cx.background_executor().clone();
959 async move {
960 let mut response_latency = None;
961 let request_start = Instant::now();
962 let task = async {
963 let mut chunks = response?.stream;
964 while let Some(chunk) = chunks.next().await {
965 if response_latency.is_none() {
966 response_latency = Some(request_start.elapsed());
967 }
968 let chunk = chunk?;
969 hunks_tx.send(chunk).await?;
970 }
971
972 anyhow::Ok(())
973 };
974
975 let result = task.await;
976
977 let error_message = result.as_ref().err().map(|error| error.to_string());
978 report_assistant_event(
979 AssistantEvent {
980 conversation_id: None,
981 kind: AssistantKind::InlineTerminal,
982 message_id,
983 phase: AssistantPhase::Response,
984 model: model_telemetry_id,
985 model_provider: model_provider_id.to_string(),
986 response_latency,
987 error_message,
988 language_name: None,
989 },
990 telemetry,
991 http_client,
992 model_api_key,
993 &executor,
994 );
995
996 result?;
997 anyhow::Ok(())
998 }
999 });
1000
1001 this.update(&mut cx, |this, _| {
1002 this.message_id = message_id;
1003 })?;
1004
1005 while let Some(hunk) = hunks_rx.next().await {
1006 this.update(&mut cx, |this, cx| {
1007 if let Some(transaction) = &mut this.transaction {
1008 transaction.push(hunk, cx);
1009 cx.notify();
1010 }
1011 })?;
1012 }
1013
1014 task.await?;
1015 anyhow::Ok(())
1016 };
1017
1018 let result = generate.await;
1019
1020 this.update(&mut cx, |this, cx| {
1021 if let Err(error) = result {
1022 this.status = CodegenStatus::Error(error);
1023 } else {
1024 this.status = CodegenStatus::Done;
1025 }
1026 cx.emit(CodegenEvent::Finished);
1027 cx.notify();
1028 })
1029 .ok();
1030 });
1031 cx.notify();
1032 }
1033
1034 pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
1035 self.status = CodegenStatus::Done;
1036 self.generation = Task::ready(());
1037 cx.emit(CodegenEvent::Finished);
1038 cx.notify();
1039 }
1040
1041 pub fn complete(&mut self, cx: &mut ModelContext<Self>) {
1042 if let Some(transaction) = self.transaction.take() {
1043 transaction.complete(cx);
1044 }
1045 }
1046
1047 pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
1048 if let Some(transaction) = self.transaction.take() {
1049 transaction.undo(cx);
1050 }
1051 }
1052}
1053
1054enum CodegenStatus {
1055 Idle,
1056 Pending,
1057 Done,
1058 Error(anyhow::Error),
1059}