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