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