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