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