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