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