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