1use crate::{
2 assistant_settings::AssistantSettings, humanize_token_count,
3 prompts::generate_terminal_assistant_prompt, AssistantPanel, AssistantPanelEvent,
4 CompletionProvider,
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, Global, Model, ModelContext,
17 Subscription, Task, TextStyle, UpdateGlobal, View, WeakView,
18};
19use language::Buffer;
20use language_model::{LanguageModelRequest, LanguageModelRequestMessage, Role};
21use settings::{update_settings_file, Settings};
22use std::{
23 cmp,
24 sync::Arc,
25 time::{Duration, Instant},
26};
27use terminal::Terminal;
28use terminal_view::TerminalView;
29use theme::ThemeSettings;
30use ui::{prelude::*, ContextMenu, IconButtonShape, PopoverMenu, Tooltip};
31use util::ResultExt;
32use workspace::{notifications::NotificationId, Toast, Workspace};
33
34pub fn init(fs: Arc<dyn Fs>, telemetry: Arc<Telemetry>, cx: &mut AppContext) {
35 cx.set_global(TerminalInlineAssistant::new(fs, telemetry));
36}
37
38const PROMPT_HISTORY_MAX_LEN: usize = 20;
39
40#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
41struct TerminalInlineAssistId(usize);
42
43impl TerminalInlineAssistId {
44 fn post_inc(&mut self) -> TerminalInlineAssistId {
45 let id = *self;
46 self.0 += 1;
47 id
48 }
49}
50
51pub struct TerminalInlineAssistant {
52 next_assist_id: TerminalInlineAssistId,
53 assists: HashMap<TerminalInlineAssistId, TerminalInlineAssist>,
54 prompt_history: VecDeque<String>,
55 telemetry: Option<Arc<Telemetry>>,
56 fs: Arc<dyn Fs>,
57}
58
59impl Global for TerminalInlineAssistant {}
60
61impl TerminalInlineAssistant {
62 pub fn new(fs: Arc<dyn Fs>, telemetry: Arc<Telemetry>) -> Self {
63 Self {
64 next_assist_id: TerminalInlineAssistId::default(),
65 assists: HashMap::default(),
66 prompt_history: VecDeque::default(),
67 telemetry: Some(telemetry),
68 fs,
69 }
70 }
71
72 pub fn assist(
73 &mut self,
74 terminal_view: &View<TerminalView>,
75 workspace: Option<WeakView<Workspace>>,
76 assistant_panel: Option<&View<AssistantPanel>>,
77 initial_prompt: Option<String>,
78 cx: &mut WindowContext,
79 ) {
80 let terminal = terminal_view.read(cx).terminal().clone();
81 let assist_id = self.next_assist_id.post_inc();
82 let prompt_buffer =
83 cx.new_model(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx));
84 let prompt_buffer = cx.new_model(|cx| MultiBuffer::singleton(prompt_buffer, cx));
85 let codegen = cx.new_model(|_| Codegen::new(terminal, self.telemetry.clone()));
86
87 let prompt_editor = cx.new_view(|cx| {
88 PromptEditor::new(
89 assist_id,
90 self.prompt_history.clone(),
91 prompt_buffer.clone(),
92 codegen,
93 assistant_panel,
94 workspace.clone(),
95 self.fs.clone(),
96 cx,
97 )
98 });
99 let prompt_editor_render = prompt_editor.clone();
100 let block = terminal_view::BlockProperties {
101 height: 2,
102 render: Box::new(move |_| prompt_editor_render.clone().into_any_element()),
103 };
104 terminal_view.update(cx, |terminal_view, cx| {
105 terminal_view.set_block_below_cursor(block, cx);
106 });
107
108 let terminal_assistant = TerminalInlineAssist::new(
109 assist_id,
110 terminal_view,
111 assistant_panel.is_some(),
112 prompt_editor,
113 workspace.clone(),
114 cx,
115 );
116
117 self.assists.insert(assist_id, terminal_assistant);
118
119 self.focus_assist(assist_id, cx);
120 }
121
122 fn focus_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
123 let assist = &self.assists[&assist_id];
124 if let Some(prompt_editor) = assist.prompt_editor.as_ref() {
125 prompt_editor.update(cx, |this, cx| {
126 this.editor.update(cx, |editor, cx| {
127 editor.focus(cx);
128 editor.select_all(&SelectAll, cx);
129 });
130 });
131 }
132 }
133
134 fn handle_prompt_editor_event(
135 &mut self,
136 prompt_editor: View<PromptEditor>,
137 event: &PromptEditorEvent,
138 cx: &mut WindowContext,
139 ) {
140 let assist_id = prompt_editor.read(cx).id;
141 match event {
142 PromptEditorEvent::StartRequested => {
143 self.start_assist(assist_id, cx);
144 }
145 PromptEditorEvent::StopRequested => {
146 self.stop_assist(assist_id, cx);
147 }
148 PromptEditorEvent::ConfirmRequested => {
149 self.finish_assist(assist_id, false, cx);
150 }
151 PromptEditorEvent::CancelRequested => {
152 self.finish_assist(assist_id, true, cx);
153 }
154 PromptEditorEvent::DismissRequested => {
155 self.dismiss_assist(assist_id, cx);
156 }
157 PromptEditorEvent::Resized { height_in_lines } => {
158 self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, cx);
159 }
160 }
161 }
162
163 fn start_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
164 let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
165 assist
166 } else {
167 return;
168 };
169
170 let Some(user_prompt) = assist
171 .prompt_editor
172 .as_ref()
173 .map(|editor| editor.read(cx).prompt(cx))
174 else {
175 return;
176 };
177
178 self.prompt_history.retain(|prompt| *prompt != user_prompt);
179 self.prompt_history.push_back(user_prompt.clone());
180 if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN {
181 self.prompt_history.pop_front();
182 }
183
184 assist
185 .terminal
186 .update(cx, |terminal, cx| {
187 terminal
188 .terminal()
189 .update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
190 })
191 .log_err();
192
193 let codegen = assist.codegen.clone();
194 let Some(request) = self.request_for_inline_assist(assist_id, cx).log_err() else {
195 return;
196 };
197
198 codegen.update(cx, |codegen, cx| codegen.start(request, cx));
199 }
200
201 fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
202 let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
203 assist
204 } else {
205 return;
206 };
207
208 assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
209 }
210
211 fn request_for_inline_assist(
212 &self,
213 assist_id: TerminalInlineAssistId,
214 cx: &mut WindowContext,
215 ) -> Result<LanguageModelRequest> {
216 let assist = self.assists.get(&assist_id).context("invalid assist")?;
217
218 let model = CompletionProvider::global(cx).model();
219
220 let shell = std::env::var("SHELL").ok();
221 let working_directory = assist
222 .terminal
223 .update(cx, |terminal, cx| {
224 terminal
225 .model()
226 .read(cx)
227 .working_directory()
228 .map(|path| path.to_string_lossy().to_string())
229 })
230 .ok()
231 .flatten();
232
233 let context_request = if assist.include_context {
234 assist.workspace.as_ref().and_then(|workspace| {
235 let workspace = workspace.upgrade()?.read(cx);
236 let assistant_panel = workspace.panel::<AssistantPanel>(cx)?;
237 Some(
238 assistant_panel
239 .read(cx)
240 .active_context(cx)?
241 .read(cx)
242 .to_completion_request(cx),
243 )
244 })
245 } else {
246 None
247 };
248
249 let prompt = generate_terminal_assistant_prompt(
250 &assist
251 .prompt_editor
252 .clone()
253 .context("invalid assist")?
254 .read(cx)
255 .prompt(cx),
256 shell.as_deref(),
257 working_directory.as_deref(),
258 );
259
260 let mut messages = Vec::new();
261 if let Some(context_request) = context_request {
262 messages = context_request.messages;
263 }
264
265 messages.push(LanguageModelRequestMessage {
266 role: Role::User,
267 content: prompt,
268 });
269
270 Ok(LanguageModelRequest {
271 model,
272 messages,
273 stop: Vec::new(),
274 temperature: 1.0,
275 })
276 }
277
278 fn finish_assist(
279 &mut self,
280 assist_id: TerminalInlineAssistId,
281 undo: bool,
282 cx: &mut WindowContext,
283 ) {
284 self.dismiss_assist(assist_id, cx);
285
286 if let Some(assist) = self.assists.remove(&assist_id) {
287 assist
288 .terminal
289 .update(cx, |this, cx| {
290 this.clear_block_below_cursor(cx);
291 this.focus_handle(cx).focus(cx);
292 })
293 .log_err();
294 assist.codegen.update(cx, |codegen, cx| {
295 if undo {
296 codegen.undo(cx);
297 } else {
298 codegen.complete(cx);
299 }
300 });
301 }
302 }
303
304 fn dismiss_assist(
305 &mut self,
306 assist_id: TerminalInlineAssistId,
307 cx: &mut WindowContext,
308 ) -> bool {
309 let Some(assist) = self.assists.get_mut(&assist_id) else {
310 return false;
311 };
312 if assist.prompt_editor.is_none() {
313 return false;
314 }
315 assist.prompt_editor = None;
316 assist
317 .terminal
318 .update(cx, |this, cx| {
319 this.clear_block_below_cursor(cx);
320 this.focus_handle(cx).focus(cx);
321 })
322 .is_ok()
323 }
324
325 fn insert_prompt_editor_into_terminal(
326 &mut self,
327 assist_id: TerminalInlineAssistId,
328 height: u8,
329 cx: &mut WindowContext,
330 ) {
331 if let Some(assist) = self.assists.get_mut(&assist_id) {
332 if let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned() {
333 assist
334 .terminal
335 .update(cx, |terminal, cx| {
336 terminal.clear_block_below_cursor(cx);
337 let block = terminal_view::BlockProperties {
338 height,
339 render: Box::new(move |_| prompt_editor.clone().into_any_element()),
340 };
341 terminal.set_block_below_cursor(block, cx);
342 })
343 .log_err();
344 }
345 }
346 }
347}
348
349struct TerminalInlineAssist {
350 terminal: WeakView<TerminalView>,
351 prompt_editor: Option<View<PromptEditor>>,
352 codegen: Model<Codegen>,
353 workspace: Option<WeakView<Workspace>>,
354 include_context: bool,
355 _subscriptions: Vec<Subscription>,
356}
357
358impl TerminalInlineAssist {
359 pub fn new(
360 assist_id: TerminalInlineAssistId,
361 terminal: &View<TerminalView>,
362 include_context: bool,
363 prompt_editor: View<PromptEditor>,
364 workspace: Option<WeakView<Workspace>>,
365 cx: &mut WindowContext,
366 ) -> Self {
367 let codegen = prompt_editor.read(cx).codegen.clone();
368 Self {
369 terminal: terminal.downgrade(),
370 prompt_editor: Some(prompt_editor.clone()),
371 codegen: codegen.clone(),
372 workspace: workspace.clone(),
373 include_context,
374 _subscriptions: vec![
375 cx.subscribe(&prompt_editor, |prompt_editor, event, cx| {
376 TerminalInlineAssistant::update_global(cx, |this, cx| {
377 this.handle_prompt_editor_event(prompt_editor, event, cx)
378 })
379 }),
380 cx.subscribe(&codegen, move |codegen, event, cx| {
381 TerminalInlineAssistant::update_global(cx, |this, cx| match event {
382 CodegenEvent::Finished => {
383 let assist = if let Some(assist) = this.assists.get(&assist_id) {
384 assist
385 } else {
386 return;
387 };
388
389 if let CodegenStatus::Error(error) = &codegen.read(cx).status {
390 if assist.prompt_editor.is_none() {
391 if let Some(workspace) = assist
392 .workspace
393 .as_ref()
394 .and_then(|workspace| workspace.upgrade())
395 {
396 let error =
397 format!("Terminal inline assistant error: {}", error);
398 workspace.update(cx, |workspace, cx| {
399 struct InlineAssistantError;
400
401 let id =
402 NotificationId::identified::<InlineAssistantError>(
403 assist_id.0,
404 );
405
406 workspace.show_toast(Toast::new(id, error), cx);
407 })
408 }
409 }
410 }
411
412 if assist.prompt_editor.is_none() {
413 this.finish_assist(assist_id, false, cx);
414 }
415 }
416 })
417 }),
418 ],
419 }
420 }
421}
422
423enum PromptEditorEvent {
424 StartRequested,
425 StopRequested,
426 ConfirmRequested,
427 CancelRequested,
428 DismissRequested,
429 Resized { height_in_lines: u8 },
430}
431
432struct PromptEditor {
433 id: TerminalInlineAssistId,
434 fs: Arc<dyn Fs>,
435 height_in_lines: u8,
436 editor: View<Editor>,
437 edited_since_done: bool,
438 prompt_history: VecDeque<String>,
439 prompt_history_ix: Option<usize>,
440 pending_prompt: String,
441 codegen: Model<Codegen>,
442 _codegen_subscription: Subscription,
443 editor_subscriptions: Vec<Subscription>,
444 pending_token_count: Task<Result<()>>,
445 token_count: Option<usize>,
446 _token_count_subscriptions: Vec<Subscription>,
447 workspace: Option<WeakView<Workspace>>,
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 fs = self.fs.clone();
455
456 let buttons = match &self.codegen.read(cx).status {
457 CodegenStatus::Idle => {
458 vec![
459 IconButton::new("cancel", IconName::Close)
460 .icon_color(Color::Muted)
461 .size(ButtonSize::None)
462 .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
463 .on_click(
464 cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
465 ),
466 IconButton::new("start", IconName::Sparkle)
467 .icon_color(Color::Muted)
468 .size(ButtonSize::None)
469 .icon_size(IconSize::XSmall)
470 .tooltip(|cx| Tooltip::for_action("Generate", &menu::Confirm, cx))
471 .on_click(
472 cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
473 ),
474 ]
475 }
476 CodegenStatus::Pending => {
477 vec![
478 IconButton::new("cancel", IconName::Close)
479 .icon_color(Color::Muted)
480 .size(ButtonSize::None)
481 .tooltip(|cx| Tooltip::text("Cancel Assist", cx))
482 .on_click(
483 cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
484 ),
485 IconButton::new("stop", IconName::Stop)
486 .icon_color(Color::Error)
487 .size(ButtonSize::None)
488 .icon_size(IconSize::XSmall)
489 .tooltip(|cx| {
490 Tooltip::with_meta(
491 "Interrupt Generation",
492 Some(&menu::Cancel),
493 "Changes won't be discarded",
494 cx,
495 )
496 })
497 .on_click(
498 cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)),
499 ),
500 ]
501 }
502 CodegenStatus::Error(_) | CodegenStatus::Done => {
503 vec![
504 IconButton::new("cancel", IconName::Close)
505 .icon_color(Color::Muted)
506 .size(ButtonSize::None)
507 .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
508 .on_click(
509 cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
510 ),
511 if self.edited_since_done {
512 IconButton::new("restart", IconName::RotateCw)
513 .icon_color(Color::Info)
514 .icon_size(IconSize::XSmall)
515 .size(ButtonSize::None)
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 } else {
528 IconButton::new("confirm", IconName::Play)
529 .icon_color(Color::Info)
530 .size(ButtonSize::None)
531 .tooltip(|cx| {
532 Tooltip::for_action("Execute generated command", &menu::Confirm, cx)
533 })
534 .on_click(cx.listener(|_, _, cx| {
535 cx.emit(PromptEditorEvent::ConfirmRequested);
536 }))
537 },
538 ]
539 }
540 };
541
542 h_flex()
543 .bg(cx.theme().colors().editor_background)
544 .border_y_1()
545 .border_color(cx.theme().status().info_border)
546 .py_1p5()
547 .h_full()
548 .w_full()
549 .on_action(cx.listener(Self::confirm))
550 .on_action(cx.listener(Self::cancel))
551 .on_action(cx.listener(Self::move_up))
552 .on_action(cx.listener(Self::move_down))
553 .child(
554 h_flex()
555 .w_12()
556 .justify_center()
557 .gap_2()
558 .child(
559 PopoverMenu::new("model-switcher")
560 .menu(move |cx| {
561 ContextMenu::build(cx, |mut menu, cx| {
562 for model in CompletionProvider::global(cx).available_models() {
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 .shape(IconButtonShape::Square)
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 "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: settings.ui_font.weight,
949 line_height: relative(1.3),
950 ..Default::default()
951 };
952 EditorElement::new(
953 &self.editor,
954 EditorStyle {
955 background: cx.theme().colors().editor_background,
956 local_player: cx.theme().players().local(),
957 text: text_style,
958 ..Default::default()
959 },
960 )
961 }
962}
963
964#[derive(Debug)]
965pub enum CodegenEvent {
966 Finished,
967}
968
969impl EventEmitter<CodegenEvent> for Codegen {}
970
971const CLEAR_INPUT: &str = "\x15";
972const CARRIAGE_RETURN: &str = "\x0d";
973
974struct TerminalTransaction {
975 terminal: Model<Terminal>,
976}
977
978impl TerminalTransaction {
979 pub fn start(terminal: Model<Terminal>) -> Self {
980 Self { terminal }
981 }
982
983 pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
984 // Ensure that the assistant cannot accidently execute commands that are streamed into the terminal
985 let input = hunk.replace(CARRIAGE_RETURN, " ");
986 self.terminal
987 .update(cx, |terminal, _| terminal.input(input));
988 }
989
990 pub fn undo(&self, cx: &mut AppContext) {
991 self.terminal
992 .update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
993 }
994
995 pub fn complete(&self, cx: &mut AppContext) {
996 self.terminal.update(cx, |terminal, _| {
997 terminal.input(CARRIAGE_RETURN.to_string())
998 });
999 }
1000}
1001
1002pub struct Codegen {
1003 status: CodegenStatus,
1004 telemetry: Option<Arc<Telemetry>>,
1005 terminal: Model<Terminal>,
1006 generation: Task<()>,
1007 transaction: Option<TerminalTransaction>,
1008}
1009
1010impl Codegen {
1011 pub fn new(terminal: Model<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
1012 Self {
1013 terminal,
1014 telemetry,
1015 status: CodegenStatus::Idle,
1016 generation: Task::ready(()),
1017 transaction: None,
1018 }
1019 }
1020
1021 pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
1022 self.status = CodegenStatus::Pending;
1023 self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
1024
1025 let telemetry = self.telemetry.clone();
1026 let model_telemetry_id = prompt.model.telemetry_id();
1027 let response = CompletionProvider::global(cx).stream_completion(prompt, cx);
1028
1029 self.generation = cx.spawn(|this, mut cx| async move {
1030 let response = response.await;
1031 let generate = async {
1032 let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
1033
1034 let task = cx.background_executor().spawn(async move {
1035 let mut response_latency = None;
1036 let request_start = Instant::now();
1037 let task = async {
1038 let mut chunks = response?;
1039 while let Some(chunk) = chunks.next().await {
1040 if response_latency.is_none() {
1041 response_latency = Some(request_start.elapsed());
1042 }
1043 let chunk = chunk?;
1044 hunks_tx.send(chunk).await?;
1045 }
1046
1047 anyhow::Ok(())
1048 };
1049
1050 let result = task.await;
1051
1052 let error_message = result.as_ref().err().map(|error| error.to_string());
1053 if let Some(telemetry) = telemetry {
1054 telemetry.report_assistant_event(
1055 None,
1056 telemetry_events::AssistantKind::Inline,
1057 model_telemetry_id,
1058 response_latency,
1059 error_message,
1060 );
1061 }
1062
1063 result?;
1064 anyhow::Ok(())
1065 });
1066
1067 while let Some(hunk) = hunks_rx.next().await {
1068 this.update(&mut cx, |this, cx| {
1069 if let Some(transaction) = &mut this.transaction {
1070 transaction.push(hunk, cx);
1071 cx.notify();
1072 }
1073 })?;
1074 }
1075
1076 task.await?;
1077 anyhow::Ok(())
1078 };
1079
1080 let result = generate.await;
1081
1082 this.update(&mut cx, |this, cx| {
1083 if let Err(error) = result {
1084 this.status = CodegenStatus::Error(error);
1085 } else {
1086 this.status = CodegenStatus::Done;
1087 }
1088 cx.emit(CodegenEvent::Finished);
1089 cx.notify();
1090 })
1091 .ok();
1092 });
1093 cx.notify();
1094 }
1095
1096 pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
1097 self.status = CodegenStatus::Done;
1098 self.generation = Task::ready(());
1099 cx.emit(CodegenEvent::Finished);
1100 cx.notify();
1101 }
1102
1103 pub fn complete(&mut self, cx: &mut ModelContext<Self>) {
1104 if let Some(transaction) = self.transaction.take() {
1105 transaction.complete(cx);
1106 }
1107 }
1108
1109 pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
1110 if let Some(transaction) = self.transaction.take() {
1111 transaction.undo(cx);
1112 }
1113 }
1114}
1115
1116enum CodegenStatus {
1117 Idle,
1118 Pending,
1119 Done,
1120 Error(anyhow::Error),
1121}