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;
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::*, 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 fs: Arc<dyn Fs>,
480 height_in_lines: u8,
481 editor: View<Editor>,
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(LanguageModelSelector::new(
618 {
619 let fs = self.fs.clone();
620 move |model, cx| {
621 update_settings_file::<AssistantSettings>(
622 fs.clone(),
623 cx,
624 move |settings, _| settings.set_model(model.clone()),
625 );
626 }
627 },
628 IconButton::new("context", IconName::SettingsAlt)
629 .shape(IconButtonShape::Square)
630 .icon_size(IconSize::Small)
631 .icon_color(Color::Muted)
632 .tooltip(move |cx| {
633 Tooltip::with_meta(
634 format!(
635 "Using {}",
636 LanguageModelRegistry::read_global(cx)
637 .active_model()
638 .map(|model| model.name().0)
639 .unwrap_or_else(|| "No model selected".into()),
640 ),
641 None,
642 "Change Model",
643 cx,
644 )
645 }),
646 ))
647 .children(
648 if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
649 let error_message = SharedString::from(error.to_string());
650 Some(
651 div()
652 .id("error")
653 .tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
654 .child(
655 Icon::new(IconName::XCircle)
656 .size(IconSize::Small)
657 .color(Color::Error),
658 ),
659 )
660 } else {
661 None
662 },
663 ),
664 )
665 .child(div().flex_1().child(self.render_prompt_editor(cx)))
666 .child(
667 h_flex()
668 .gap_1()
669 .pr_4()
670 .children(self.render_token_count(cx))
671 .children(buttons),
672 )
673 }
674}
675
676impl FocusableView for PromptEditor {
677 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
678 self.editor.focus_handle(cx)
679 }
680}
681
682impl PromptEditor {
683 const MAX_LINES: u8 = 8;
684
685 #[allow(clippy::too_many_arguments)]
686 fn new(
687 id: TerminalInlineAssistId,
688 prompt_history: VecDeque<String>,
689 prompt_buffer: Model<MultiBuffer>,
690 codegen: Model<Codegen>,
691 assistant_panel: Option<&View<AssistantPanel>>,
692 workspace: Option<WeakView<Workspace>>,
693 fs: Arc<dyn Fs>,
694 cx: &mut ViewContext<Self>,
695 ) -> Self {
696 let prompt_editor = cx.new_view(|cx| {
697 let mut editor = Editor::new(
698 EditorMode::AutoHeight {
699 max_lines: Self::MAX_LINES as usize,
700 },
701 prompt_buffer,
702 None,
703 false,
704 cx,
705 );
706 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
707 editor.set_placeholder_text("Add a prompt…", cx);
708 editor
709 });
710
711 let mut token_count_subscriptions = Vec::new();
712 if let Some(assistant_panel) = assistant_panel {
713 token_count_subscriptions
714 .push(cx.subscribe(assistant_panel, Self::handle_assistant_panel_event));
715 }
716
717 let mut this = Self {
718 id,
719 height_in_lines: 1,
720 editor: prompt_editor,
721 edited_since_done: false,
722 prompt_history,
723 prompt_history_ix: None,
724 pending_prompt: String::new(),
725 _codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
726 editor_subscriptions: Vec::new(),
727 codegen,
728 fs,
729 pending_token_count: Task::ready(Ok(())),
730 token_count: None,
731 _token_count_subscriptions: token_count_subscriptions,
732 workspace,
733 };
734 this.count_lines(cx);
735 this.count_tokens(cx);
736 this.subscribe_to_editor(cx);
737 this
738 }
739
740 fn subscribe_to_editor(&mut self, cx: &mut ViewContext<Self>) {
741 self.editor_subscriptions.clear();
742 self.editor_subscriptions
743 .push(cx.observe(&self.editor, Self::handle_prompt_editor_changed));
744 self.editor_subscriptions
745 .push(cx.subscribe(&self.editor, Self::handle_prompt_editor_events));
746 }
747
748 fn prompt(&self, cx: &AppContext) -> String {
749 self.editor.read(cx).text(cx)
750 }
751
752 fn count_lines(&mut self, cx: &mut ViewContext<Self>) {
753 let height_in_lines = cmp::max(
754 2, // Make the editor at least two lines tall, to account for padding and buttons.
755 cmp::min(
756 self.editor
757 .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
758 Self::MAX_LINES as u32,
759 ),
760 ) as u8;
761
762 if height_in_lines != self.height_in_lines {
763 self.height_in_lines = height_in_lines;
764 cx.emit(PromptEditorEvent::Resized { height_in_lines });
765 }
766 }
767
768 fn handle_assistant_panel_event(
769 &mut self,
770 _: View<AssistantPanel>,
771 event: &AssistantPanelEvent,
772 cx: &mut ViewContext<Self>,
773 ) {
774 let AssistantPanelEvent::ContextEdited { .. } = event;
775 self.count_tokens(cx);
776 }
777
778 fn count_tokens(&mut self, cx: &mut ViewContext<Self>) {
779 let assist_id = self.id;
780 let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
781 return;
782 };
783 self.pending_token_count = cx.spawn(|this, mut cx| async move {
784 cx.background_executor().timer(Duration::from_secs(1)).await;
785 let request =
786 cx.update_global(|inline_assistant: &mut TerminalInlineAssistant, cx| {
787 inline_assistant.request_for_inline_assist(assist_id, cx)
788 })??;
789
790 let token_count = cx.update(|cx| model.count_tokens(request, cx))?.await?;
791 this.update(&mut cx, |this, cx| {
792 this.token_count = Some(token_count);
793 cx.notify();
794 })
795 })
796 }
797
798 fn handle_prompt_editor_changed(&mut self, _: View<Editor>, cx: &mut ViewContext<Self>) {
799 self.count_lines(cx);
800 }
801
802 fn handle_prompt_editor_events(
803 &mut self,
804 _: View<Editor>,
805 event: &EditorEvent,
806 cx: &mut ViewContext<Self>,
807 ) {
808 match event {
809 EditorEvent::Edited { .. } => {
810 let prompt = self.editor.read(cx).text(cx);
811 if self
812 .prompt_history_ix
813 .map_or(true, |ix| self.prompt_history[ix] != prompt)
814 {
815 self.prompt_history_ix.take();
816 self.pending_prompt = prompt;
817 }
818
819 self.edited_since_done = true;
820 cx.notify();
821 }
822 EditorEvent::BufferEdited => {
823 self.count_tokens(cx);
824 }
825 _ => {}
826 }
827 }
828
829 fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
830 match &self.codegen.read(cx).status {
831 CodegenStatus::Idle => {
832 self.editor
833 .update(cx, |editor, _| editor.set_read_only(false));
834 }
835 CodegenStatus::Pending => {
836 self.editor
837 .update(cx, |editor, _| editor.set_read_only(true));
838 }
839 CodegenStatus::Done | CodegenStatus::Error(_) => {
840 self.edited_since_done = false;
841 self.editor
842 .update(cx, |editor, _| editor.set_read_only(false));
843 }
844 }
845 }
846
847 fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
848 match &self.codegen.read(cx).status {
849 CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
850 cx.emit(PromptEditorEvent::CancelRequested);
851 }
852 CodegenStatus::Pending => {
853 cx.emit(PromptEditorEvent::StopRequested);
854 }
855 }
856 }
857
858 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
859 match &self.codegen.read(cx).status {
860 CodegenStatus::Idle => {
861 if !self.editor.read(cx).text(cx).trim().is_empty() {
862 cx.emit(PromptEditorEvent::StartRequested);
863 }
864 }
865 CodegenStatus::Pending => {
866 cx.emit(PromptEditorEvent::DismissRequested);
867 }
868 CodegenStatus::Done => {
869 if self.edited_since_done {
870 cx.emit(PromptEditorEvent::StartRequested);
871 } else {
872 cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
873 }
874 }
875 CodegenStatus::Error(_) => {
876 cx.emit(PromptEditorEvent::StartRequested);
877 }
878 }
879 }
880
881 fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
882 if matches!(self.codegen.read(cx).status, CodegenStatus::Done) {
883 cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
884 }
885 }
886
887 fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
888 if let Some(ix) = self.prompt_history_ix {
889 if ix > 0 {
890 self.prompt_history_ix = Some(ix - 1);
891 let prompt = self.prompt_history[ix - 1].as_str();
892 self.editor.update(cx, |editor, cx| {
893 editor.set_text(prompt, cx);
894 editor.move_to_beginning(&Default::default(), cx);
895 });
896 }
897 } else if !self.prompt_history.is_empty() {
898 self.prompt_history_ix = Some(self.prompt_history.len() - 1);
899 let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
900 self.editor.update(cx, |editor, cx| {
901 editor.set_text(prompt, cx);
902 editor.move_to_beginning(&Default::default(), cx);
903 });
904 }
905 }
906
907 fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
908 if let Some(ix) = self.prompt_history_ix {
909 if ix < self.prompt_history.len() - 1 {
910 self.prompt_history_ix = Some(ix + 1);
911 let prompt = self.prompt_history[ix + 1].as_str();
912 self.editor.update(cx, |editor, cx| {
913 editor.set_text(prompt, cx);
914 editor.move_to_end(&Default::default(), cx)
915 });
916 } else {
917 self.prompt_history_ix = None;
918 let prompt = self.pending_prompt.as_str();
919 self.editor.update(cx, |editor, cx| {
920 editor.set_text(prompt, cx);
921 editor.move_to_end(&Default::default(), cx)
922 });
923 }
924 }
925 }
926
927 fn render_token_count(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
928 let model = LanguageModelRegistry::read_global(cx).active_model()?;
929 let token_count = self.token_count?;
930 let max_token_count = model.max_token_count();
931
932 let remaining_tokens = max_token_count as isize - token_count as isize;
933 let token_count_color = if remaining_tokens <= 0 {
934 Color::Error
935 } else if token_count as f32 / max_token_count as f32 >= 0.8 {
936 Color::Warning
937 } else {
938 Color::Muted
939 };
940
941 let mut token_count = h_flex()
942 .id("token_count")
943 .gap_0p5()
944 .child(
945 Label::new(humanize_token_count(token_count))
946 .size(LabelSize::Small)
947 .color(token_count_color),
948 )
949 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
950 .child(
951 Label::new(humanize_token_count(max_token_count))
952 .size(LabelSize::Small)
953 .color(Color::Muted),
954 );
955 if let Some(workspace) = self.workspace.clone() {
956 token_count = token_count
957 .tooltip(|cx| {
958 Tooltip::with_meta(
959 "Tokens Used by Inline Assistant",
960 None,
961 "Click to Open Assistant Panel",
962 cx,
963 )
964 })
965 .cursor_pointer()
966 .on_mouse_down(gpui::MouseButton::Left, |_, cx| cx.stop_propagation())
967 .on_click(move |_, cx| {
968 cx.stop_propagation();
969 workspace
970 .update(cx, |workspace, cx| {
971 workspace.focus_panel::<AssistantPanel>(cx)
972 })
973 .ok();
974 });
975 } else {
976 token_count = token_count
977 .cursor_default()
978 .tooltip(|cx| Tooltip::text("Tokens Used by Inline Assistant", cx));
979 }
980
981 Some(token_count)
982 }
983
984 fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
985 let settings = ThemeSettings::get_global(cx);
986 let text_style = TextStyle {
987 color: if self.editor.read(cx).read_only(cx) {
988 cx.theme().colors().text_disabled
989 } else {
990 cx.theme().colors().text
991 },
992 font_family: settings.buffer_font.family.clone(),
993 font_fallbacks: settings.buffer_font.fallbacks.clone(),
994 font_size: settings.buffer_font_size.into(),
995 font_weight: settings.buffer_font.weight,
996 line_height: relative(settings.buffer_line_height.value()),
997 ..Default::default()
998 };
999 EditorElement::new(
1000 &self.editor,
1001 EditorStyle {
1002 background: cx.theme().colors().editor_background,
1003 local_player: cx.theme().players().local(),
1004 text: text_style,
1005 ..Default::default()
1006 },
1007 )
1008 }
1009}
1010
1011#[derive(Debug)]
1012pub enum CodegenEvent {
1013 Finished,
1014}
1015
1016impl EventEmitter<CodegenEvent> for Codegen {}
1017
1018const CLEAR_INPUT: &str = "\x15";
1019const CARRIAGE_RETURN: &str = "\x0d";
1020
1021struct TerminalTransaction {
1022 terminal: Model<Terminal>,
1023}
1024
1025impl TerminalTransaction {
1026 pub fn start(terminal: Model<Terminal>) -> Self {
1027 Self { terminal }
1028 }
1029
1030 pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
1031 // Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
1032 let input = Self::sanitize_input(hunk);
1033 self.terminal
1034 .update(cx, |terminal, _| terminal.input(input));
1035 }
1036
1037 pub fn undo(&self, cx: &mut AppContext) {
1038 self.terminal
1039 .update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
1040 }
1041
1042 pub fn complete(&self, cx: &mut AppContext) {
1043 self.terminal.update(cx, |terminal, _| {
1044 terminal.input(CARRIAGE_RETURN.to_string())
1045 });
1046 }
1047
1048 fn sanitize_input(input: String) -> String {
1049 input.replace(['\r', '\n'], "")
1050 }
1051}
1052
1053pub struct Codegen {
1054 status: CodegenStatus,
1055 telemetry: Option<Arc<Telemetry>>,
1056 terminal: Model<Terminal>,
1057 generation: Task<()>,
1058 message_id: Option<String>,
1059 transaction: Option<TerminalTransaction>,
1060}
1061
1062impl Codegen {
1063 pub fn new(terminal: Model<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
1064 Self {
1065 terminal,
1066 telemetry,
1067 status: CodegenStatus::Idle,
1068 generation: Task::ready(()),
1069 message_id: None,
1070 transaction: None,
1071 }
1072 }
1073
1074 pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
1075 let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
1076 return;
1077 };
1078
1079 let model_api_key = model.api_key(cx);
1080 let http_client = cx.http_client();
1081 let telemetry = self.telemetry.clone();
1082 self.status = CodegenStatus::Pending;
1083 self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
1084 self.generation = cx.spawn(|this, mut cx| async move {
1085 let model_telemetry_id = model.telemetry_id();
1086 let model_provider_id = model.provider_id();
1087 let response = model.stream_completion_text(prompt, &cx).await;
1088 let generate = async {
1089 let message_id = response
1090 .as_ref()
1091 .ok()
1092 .and_then(|response| response.message_id.clone());
1093
1094 let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
1095
1096 let task = cx.background_executor().spawn({
1097 let message_id = message_id.clone();
1098 let executor = cx.background_executor().clone();
1099 async move {
1100 let mut response_latency = None;
1101 let request_start = Instant::now();
1102 let task = async {
1103 let mut chunks = response?.stream;
1104 while let Some(chunk) = chunks.next().await {
1105 if response_latency.is_none() {
1106 response_latency = Some(request_start.elapsed());
1107 }
1108 let chunk = chunk?;
1109 hunks_tx.send(chunk).await?;
1110 }
1111
1112 anyhow::Ok(())
1113 };
1114
1115 let result = task.await;
1116
1117 let error_message = result.as_ref().err().map(|error| error.to_string());
1118 report_assistant_event(
1119 AssistantEvent {
1120 conversation_id: None,
1121 kind: AssistantKind::InlineTerminal,
1122 message_id,
1123 phase: AssistantPhase::Response,
1124 model: model_telemetry_id,
1125 model_provider: model_provider_id.to_string(),
1126 response_latency,
1127 error_message,
1128 language_name: None,
1129 },
1130 telemetry,
1131 http_client,
1132 model_api_key,
1133 &executor,
1134 );
1135
1136 result?;
1137 anyhow::Ok(())
1138 }
1139 });
1140
1141 this.update(&mut cx, |this, _| {
1142 this.message_id = message_id;
1143 })?;
1144
1145 while let Some(hunk) = hunks_rx.next().await {
1146 this.update(&mut cx, |this, cx| {
1147 if let Some(transaction) = &mut this.transaction {
1148 transaction.push(hunk, cx);
1149 cx.notify();
1150 }
1151 })?;
1152 }
1153
1154 task.await?;
1155 anyhow::Ok(())
1156 };
1157
1158 let result = generate.await;
1159
1160 this.update(&mut cx, |this, cx| {
1161 if let Err(error) = result {
1162 this.status = CodegenStatus::Error(error);
1163 } else {
1164 this.status = CodegenStatus::Done;
1165 }
1166 cx.emit(CodegenEvent::Finished);
1167 cx.notify();
1168 })
1169 .ok();
1170 });
1171 cx.notify();
1172 }
1173
1174 pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
1175 self.status = CodegenStatus::Done;
1176 self.generation = Task::ready(());
1177 cx.emit(CodegenEvent::Finished);
1178 cx.notify();
1179 }
1180
1181 pub fn complete(&mut self, cx: &mut ModelContext<Self>) {
1182 if let Some(transaction) = self.transaction.take() {
1183 transaction.complete(cx);
1184 }
1185 }
1186
1187 pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
1188 if let Some(transaction) = self.transaction.take() {
1189 transaction.undo(cx);
1190 }
1191 }
1192}
1193
1194enum CodegenStatus {
1195 Idle,
1196 Pending,
1197 Done,
1198 Error(anyhow::Error),
1199}