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