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